UNPKG

art-standard-lib

Version:

The Standard Library for JavaScript that aught to be.

260 lines (219 loc) 8.55 kB
{isPlainObject, mergeInto, isString, isFunction, isObject, isPlainArray, isJsonAtomicType} = Core = require './Core' module.exports = class Types mergeInto @, Core.Types throwInequalityError = (a, b) -> throw new Error "Value types are not compatible for inequality tests. a: #{a?.constructor.name}, b: #{b?.constructor.name}" # 12-2017 TODO: Performance test: I think these should be as performant as just the direct operators for the normal cases. # If so, we can replace them for all operators in CaffeineScript and get operator overloading. # NOTE: a.constructor == b.constructor doesn't work across JavaScript zones (that's not the right word. but in browser, iFrames) # We may want to use: typeof a == typeof b, but that needs perf-tested too. # TODO: perfTest # Compare: # a.constructor == b.constructor # with # typeof a == typeof b # https://run.perf.zone/view/compare-type-equality-1514486807306 # Results: safari sucks for constructor comparison. Chrome, FF and Safarai are very fast for sameType # Safari, FF and Chrome are all full-speed # Edge doesn't perform at all well, but even with the simplest function it's almost the same ((a, b) -> a > b is terrible in Edge) # I think it may be a testing glitch # https://run.perf.zone/view/operator-overloading-optimized-1514491281013 # GRR - in my realworld tests, even a simplified version still is way slow in all browsers # https://run.perf.zone/view/operator-overloading-optimized-1514494600425 ### # simplified - as simple as I can make it # sacrifices some semantics gt = (a, b)-> if (o = a > b) || a <= b return o a.gt b # just a tad more complex and only sacrifices clear errors gt = (a, b)-> if typeof a == typeof b if (o = a > b) || a <= b return o a.gt b ### tEq = (a, b) -> typeof a == typeof b @gt: (a, b)-> if a? && b? then s = tEq a, b; (s && a > b) || (if (s && a <= b) then false else a.gt b) else throwInequalityError a, b @lt: (a, b)-> if a? && b? then s = tEq a, b; (s && a < b) || (if (s && a >= b) then false else a.lt b) else throwInequalityError a, b @gte: (a, b)-> if a? && b? then s = tEq a, b; (s && a >= b) || (if (s && a < b) then false else a.gte b) else throwInequalityError a, b @lte: (a, b)-> if a? && b? then s = tEq a, b; (s && a <= b) || (if (s && a > b) then false else a.lte b) else throwInequalityError a, b ### like RubyOnRails#present: "An object is present if it's not blank." basic: present null, undefined, false or "" returns false (or whatever returnIfNotPresent is set to) all other values return something truish - generally themselves Yes, in ruby/rails, present(false) == false. Though logically true, I've also found it lead to unexpected results in production code. I had a false-value where I was expecting a string due to a corrupted database value. Possible argment for present(false) == false: It's easy to accidentally return false when you meant to return null or undefined. I think this is how my database got the corrupted false-instead-of null-value. Another argument: present(x) should always be false if x is false. custom: for bar where isFunction bar.present present bar returns bar.present() special-case truish results: present 0 or false returns true for any other value foo, present foo returns foo IN: obj: object tested for presence returnIfNotPresent: [false] what to return if not present OUT: returnIfNotPresent, true, or the value passed in If 'obj' has method: obj.present() => obj.present() ### @present: (obj, returnIfNotPresent = false) -> present = if isFunction obj?.getPresent obj.getPresent() else if isFunction obj?.present obj.present() else if isString obj stringIsPresent obj else (obj != undefined && obj != null && obj != false) if present then obj || true else returnIfNotPresent @stringIsPresent: stringIsPresent = (str) -> isString(str) && !/^(\s+|)$/.test str @functionName: functionName = (f) -> (f.name || ((matched = "#{f}".match(/function ([a-zA-Z]+)\(/)) && matched[1]) || "function") @objectName: objectName = (obj) -> if !obj then "" + obj else if a = obj.getNamespacePath?() then a else if a = obj.classPathName then a else if obj.constructor == Object then "Object" else if isFunction obj then functionName obj else if isString(name = obj.constructor?.name) && name.length > 0 then name else if obj instanceof Object then "(anonymous instanceof Object)" else "(objectName unknown)" @isBrowserObject: (obj) => return false unless @isObject obj name = @objectName obj name.slice(0,4)=="HTML" || name.slice(0,22) == "CanvasRenderingContext" ###################### # Plain Data ###################### ### IN: f: (value, [key]) -> f is called on every non-plainObject and non-plainArray reachable by traversing the plainObject/plainArray structure If f is called on a propery of a plainObject, the key for that property is also passed in. OUT: value ### @deepEach: deepEach = (v, f, key) -> if isPlainArray v deepEach subV, f for subV in v else if isPlainObject v deepEach subV, f, k for k, subV of v else f v, key v ### deepEachAll: just like deepEach except 'f' gets called on every value found including the initial value. ### @deepEachAll: deepEachAll = (v, f, key) -> f v, key if isPlainArray v deepEachAll subV, f for subV in v else if isPlainObject v deepEachAll subV, f, k for k, subV of v else v ### only creates a new array if the children changed ### deepMapArray = (array, mapper, options) -> res = null for v, i in array r = deepMap v, mapper, options if r!=v res ||= array.slice() res[i] = r res ||= array if p = options?.postprocessArray p res else res cloneObjectUpToKey = (obj, k) -> res = {} for k2, v of obj break if k2 == k res[k2] = v res deepMapObject = (obj, mapper, options) -> res = null for k, v of obj r = deepMap v, mapper, options if r!=v || res res ||= cloneObjectUpToKey obj, k res[k] = r res ||= obj if p = options?.postprocessObject p res else res noopMapper = (v) -> v ### Applies "f" to every -value- in a nested structure of plain arrays and objects. Pure functional efficient: If an array or object, and all its sub values, didn't change, the original array/object is reused. NOTE: deepMap only yields values to 'mapper' which are NOT plain arrays nor plain objects. ### @deepMap: deepMap = (v, mapper, options) -> arrayMapper = options?.arrays || noopMapper objectMapper = options?.objects || noopMapper mapper ||= noopMapper if isPlainArray v then deepMapArray arrayMapper(v), mapper, options else if isPlainObject v then deepMapObject objectMapper(v), mapper, options else mapper v # convert structure to only built-in types. # functions are left untouched # Non-PlainObjects are converted to their objectName string @toPlainStructure: (o) -> deepMap o, (o) -> if isObject o if o.toPlainStructure o.toPlainStructure() else objectName o else o ### similar to toPlainStructure, except all non-JSON types are converted to strings ### @toJsonStructure: toJsonStructure = (o) -> deepMap o, (o) -> if isObject o if o.toJsonStructure o.toJsonStructure() else toJsonStructure if o.toPlainStructure o.toPlainStructure() else "#{o}" else if isJsonAtomicType o o else "#{o}" #https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm @toPostMessageStructure: toPostMessageStructure = (o) -> deepMap o, (o) -> switch o.constructor when ArrayBuffer, Date, RegExp, Blob, File, FileList, ImageData, Boolean, String o else if isObject o if o.toPostMessageStructure o.toPostMessageStructure() else if o.toPlainStructure toPostMessageStructure o.toPlainStructure() else "#{o}" else "#{o}"