qiprofile
Version:
Quantitative Imaging Profile ( QiPr) web application
280 lines (257 loc) • 10.5 kB
text/coffeescript
define ['angular', 'lodash', 'underscore.string', 'moment'], (ng, _, _s, moment) ->
helpers = ng.module 'qiprofile.helpers', []
helpers.factory 'StringHelper', ->
# Improves on underscore.string dasherize by converting each
# sequence of two or more capital letters to lowercase without
# dashes, e.g.:
# StringHelper.dasherize('TNM')
# returns 'tnm' rather than the underscore.string result '-t-n-m'.
#
# @param s the input string
# @returns the lowercase dashed conversion
dasherize: (s) ->
# Prepares the given match for underscore.string dasherize.
prep = (match) ->
first = match[0]
rest = match.substring(1)
last = match[match.length - 1]
if last == last.toUpperCase()
first + rest.toLowerCase()
else
first + rest[...-2].toLowerCase() + rest[-2..]
# Prepare the input string.
prepped = s.replace(/[A-Z]{2,}.?/g, prep)
# Delegate to underscore.string.
sdashed = _s.dasherize(prepped)
# Remove a bogus leading dash, if necessary.
if sdashed[0] == '-' and s[0] != '-'
sdashed.substring(1)
else
sdashed
helpers.factory 'ObjectHelper', ->
# Define here for reuse below.
_sortValuesByKey = (obj) ->
if obj?
# The sorted keys.
keys = _.keys(obj).sort()
# Return the values sorted by key.
(obj[key] for key in keys)
else
[]
# @param key the property name
# @param value the property value
# @returns the {key, value} object
associate: (key, value) ->
obj = {}
obj[key] = value
obj
# @param objects the delegate objects
# @returns a new object with properties from the given objects
delegate: (objects...) ->
_.reduce(objects, _.defaults, {})
# Pretty prints the given object in a readable format. This function
# handles cycles by substituting an elipsis ('...') if a referenced
# object has already been visited.
#
# @param obj the object to print
# @returns the string representation
prettyPrint: (obj) ->
# Adapted from
# http://stackoverflow.com/questions/957537/how-can-i-print-a-javascript-object
# and
# //http://stackoverflow.com/questions/9382167/serializing-object-that-contains-cyclic-object-value.
visited = []
replace = (key, val) ->
elideCycles = (val) ->
if _.isObject(val)
if _.isFunction(val) or val in visited
'...'
else
visited.push(val)
val
else
val
elideCycles(val)
JSON.stringify(obj, replace, 2)
# @param obj the associative object
# @returns the concatenated values array
sortValuesByKey: _sortValuesByKey
# @param objects the objects from which to collect the values
# @param accessor the associative object accessor callback or
# property name
# @returns the concatenated value objects
collectValues: (objects, accessor) ->
# Concatenates the given current associative object
# values sorted by the associative keys to the given
# accumulator array.
#
# @param accumulator the previous result array
# @param current the current associative object
# @returns the concatenated results array
concatValues = (accumulator, current) ->
values = _sortValuesByKey(current)
accumulator.concat(values)
# The associative objects.
assocObjs = _.map(objects, accessor)
# Accumulate the values.
assocObjs.reduce(concatValues, [])
# @param obj the first object to compare
# @param other the second object to compare
# @returns whether both objects have the same properties and
# the corresponding property values are equal
propertiesEqual: (obj, other) ->
# Group by property name.
props = _.unique(_.keys(obj).concat(_.keys(other)))
pairs = ([obj[prop], other[prop]] for prop in props)
# Return whether all value pairs are equal.
_.every(pairs, (pair) -> pair[0] == pair[1])
# Aliases the source object properties which are not already
# defined in the destination object.
#
# @param source the copy source object
# @param dest the copy destination object
# @param filter an optional function that filters the
# properties to alias
aliasProperties: (source, dest, filter=null) ->
# @param prop the source property to alias in the destination
defineAlias = (prop) ->
Object.defineProperty dest, prop,
enumerable: true
get: -> source[prop]
set: (val) -> source[prop] = val
# The properties to alias.
srcProps = Object.getOwnPropertyNames(source)
destProps = Object.getOwnPropertyNames(dest)
aliasProps = _.difference(srcProps, destProps)
if filter
aliasProps = aliasProps.filter(filter)
# Make the virtual properties.
# Note: each property must be defined in a call to the defineAlias
# function. That function body cannot be inlined in the loop below
# since CoffeeScript defines the iteration variable prop in a scope
# outside of the loop, which in turn implies that the alias getter
# and setter have a function closure which refers to a shared prop
# variable which will resolve to the last iteration value.
for prop in aliasProps
defineAlias(prop)
# Aliases the source object properties which are not already
# defined in the destination object and satisfy thew following
# conditions:
# * the property name does not begin with an underscore (_)
# or dollar sign ($)
# * the source property value is not a function
#
# @param source the copy source object
# @param dest the copy destination object
aliasPublicDataProperties: (source, dest) ->
# Filters the given property to select only non-functions
# whose names do not begin with _ or $.
filter = (prop) ->
prop[0] not in '_$' and not _.isFunction(source[prop])
# Delegate to aliasProperties with the property filter.
@aliasProperties(source, dest, filter)
# Parses the JSON data into a Javascript object and creates
# camelCase property aliases for underscore property names.
# If the input data is an array Eve REST result, signified
# by the presence of an _items array property, then this
# method returns an array of Javascript objects, each of
# which is recursively transformed by this function.
#
# @param data the REST JSON data
# @returns the Javascript object
fromJson: (data) ->
# Aliases the given underscore property.
#
# @param obj the input object to modify
# @param property the input property to alias
camelizeProperty = (obj, property) ->
# The camelCase alias.
alias = property[0] + _s.camelize(property.substring(1))
# If the camelCase alias is not yet defined, then
# create the new property.
if _.isObject(obj) and not obj.hasOwnProperty(alias)
# Make the camelCase property.
Object.defineProperty obj, alias,
enumerable: true
get: -> obj[property]
set: (value) -> obj[property] = value
# Preserve the underscore property, but remove
# it from the enumerable list.
Object.defineProperty obj, property,
enumerable: false
value: obj[property]
writable: true
# Aliases underscore properties with camelCase properties.
#
# @param obj the input object to modify
# @returns the input object
camelizeProperties = (obj, visited=[]) ->
# Add camelCase aliases to underscore properties.
# Only unvisited plain objects are aliased.
if _.isObject(obj) and obj not in visited
# Mark the object as visited.
visited.push(obj)
for key, val of obj
# If the key has a non-leading underscore, then
# camelize the underscore property.
if key.lastIndexOf('_') > 0
camelizeProperty(obj, key)
# Recurse.
if _.isArray(val)
camelizeProperties(item, visited) for item in val
else
camelizeProperties(val, visited)
# Return the input object.
obj
# End of camelizeProperties.
# The initial parsed Javascript object.
obj = ng.fromJson(data)
# If the object is a REST array with _items, then return
# the camelized items. Otherwise, returns the camelized
# object.
if obj.hasOwnProperty('_items')
visited = []
camelizeProperties(item, visited) for item in obj._items
else
camelizeProperties(obj)
helpers.factory 'DateHelper', ['ObjectHelper', (ObjectHelper) ->
# Converts the input to a moment. The input can be a date string,
# e.g. '07 Sep 1986', or the number of milliseconds since
# the Unix Epoch (Jan 1 1970 12AM UTC).
#
# A string input must contain the date in the format
# DD MMM YYYY, e.g. 'Tue, 03 Feb 2012 00:00:00 GMT'.
# Otherwise, an error is thrown. Note that the time in
# the example input is ignored.
#
# @param date the date integer or string
# @returns the parsed moment date, or the input date
# if the input date is undefined or null
asMoment: (date) ->
if date?
if _.isString(date)
match = /(\d{2}) ([A-Z][a-z]{2}) (\d{4})/.exec(date)
if match
moment(match[1..3].join(' '), 'DD MMM YYYY')
else
throw new Error("Input string does not contain a date" +
" formatted as DD MMM YYYY: #{ date }")
else
moment(date)
else
date
# Anonymizes the given date by changing the month and day to
# July 7.
#
# @param date the date to anonymize
# @returns the new anonymized date, or undefined
# if the input date is undefined or null
anonymize: (date) ->
# The quirky Javascript Date month is zero-based but the
# day of month ('date' in Javascript Date's equally quirky
# naming convention) is one-based.
if date?
moment([date.year(), 6, 7])
else
date
]