README.md
1# jsprim: utilities for primitive JavaScript types
2
3This module provides miscellaneous facilities for working with strings,
4numbers, dates, and objects and arrays of these basic types.
5
6
7### deepCopy(obj)
8
9Creates a deep copy of a primitive type, object, or array of primitive types.
10
11
12### deepEqual(obj1, obj2)
13
14Returns whether two objects are equal.
15
16
17### isEmpty(obj)
18
19Returns true if the given object has no properties and false otherwise. This
20is O(1) (unlike `Object.keys(obj).length === 0`, which is O(N)).
21
22### hasKey(obj, key)
23
24Returns true if the given object has an enumerable, non-inherited property
25called `key`. [For information on enumerability and ownership of properties, see
26the MDN
27documentation.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties)
28
29### forEachKey(obj, callback)
30
31Like Array.forEach, but iterates enumerable, owned properties of an object
32rather than elements of an array. Equivalent to:
33
34 for (var key in obj) {
35 if (Object.prototype.hasOwnProperty.call(obj, key)) {
36 callback(key, obj[key]);
37 }
38 }
39
40
41### flattenObject(obj, depth)
42
43Flattens an object up to a given level of nesting, returning an array of arrays
44of length "depth + 1", where the first "depth" elements correspond to flattened
45columns and the last element contains the remaining object . For example:
46
47 flattenObject({
48 'I': {
49 'A': {
50 'i': {
51 'datum1': [ 1, 2 ],
52 'datum2': [ 3, 4 ]
53 },
54 'ii': {
55 'datum1': [ 3, 4 ]
56 }
57 },
58 'B': {
59 'i': {
60 'datum1': [ 5, 6 ]
61 },
62 'ii': {
63 'datum1': [ 7, 8 ],
64 'datum2': [ 3, 4 ],
65 },
66 'iii': {
67 }
68 }
69 },
70 'II': {
71 'A': {
72 'i': {
73 'datum1': [ 1, 2 ],
74 'datum2': [ 3, 4 ]
75 }
76 }
77 }
78 }, 3)
79
80becomes:
81
82 [
83 [ 'I', 'A', 'i', { 'datum1': [ 1, 2 ], 'datum2': [ 3, 4 ] } ],
84 [ 'I', 'A', 'ii', { 'datum1': [ 3, 4 ] } ],
85 [ 'I', 'B', 'i', { 'datum1': [ 5, 6 ] } ],
86 [ 'I', 'B', 'ii', { 'datum1': [ 7, 8 ], 'datum2': [ 3, 4 ] } ],
87 [ 'I', 'B', 'iii', {} ],
88 [ 'II', 'A', 'i', { 'datum1': [ 1, 2 ], 'datum2': [ 3, 4 ] } ]
89 ]
90
91This function is strict: "depth" must be a non-negative integer and "obj" must
92be a non-null object with at least "depth" levels of nesting under all keys.
93
94
95### flattenIter(obj, depth, func)
96
97This is similar to `flattenObject` except that instead of returning an array,
98this function invokes `func(entry)` for each `entry` in the array that
99`flattenObject` would return. `flattenIter(obj, depth, func)` is logically
100equivalent to `flattenObject(obj, depth).forEach(func)`. Importantly, this
101version never constructs the full array. Its memory usage is O(depth) rather
102than O(n) (where `n` is the number of flattened elements).
103
104There's another difference between `flattenObject` and `flattenIter` that's
105related to the special case where `depth === 0`. In this case, `flattenObject`
106omits the array wrapping `obj` (which is regrettable).
107
108
109### pluck(obj, key)
110
111Fetch nested property "key" from object "obj", traversing objects as needed.
112For example, `pluck(obj, "foo.bar.baz")` is roughly equivalent to
113`obj.foo.bar.baz`, except that:
114
1151. If traversal fails, the resulting value is undefined, and no error is
116 thrown. For example, `pluck({}, "foo.bar")` is just undefined.
1172. If "obj" has property "key" directly (without traversing), the
118 corresponding property is returned. For example,
119 `pluck({ 'foo.bar': 1 }, 'foo.bar')` is 1, not undefined. This is also
120 true recursively, so `pluck({ 'a': { 'foo.bar': 1 } }, 'a.foo.bar')` is
121 also 1, not undefined.
122
123
124### randElt(array)
125
126Returns an element from "array" selected uniformly at random. If "array" is
127empty, throws an Error.
128
129
130### startsWith(str, prefix)
131
132Returns true if the given string starts with the given prefix and false
133otherwise.
134
135
136### endsWith(str, suffix)
137
138Returns true if the given string ends with the given suffix and false
139otherwise.
140
141
142### parseInteger(str, options)
143
144Parses the contents of `str` (a string) as an integer. On success, the integer
145value is returned (as a number). On failure, an error is **returned** describing
146why parsing failed.
147
148By default, leading and trailing whitespace characters are not allowed, nor are
149trailing characters that are not part of the numeric representation. This
150behaviour can be toggled by using the options below. The empty string (`''`) is
151not considered valid input. If the return value cannot be precisely represented
152as a number (i.e., is smaller than `Number.MIN_SAFE_INTEGER` or larger than
153`Number.MAX_SAFE_INTEGER`), an error is returned. Additionally, the string
154`'-0'` will be parsed as the integer `0`, instead of as the IEEE floating point
155value `-0`.
156
157This function accepts both upper and lowercase characters for digits, similar to
158`parseInt()`, `Number()`, and [strtol(3C)](https://illumos.org/man/3C/strtol).
159
160The following may be specified in `options`:
161
162Option | Type | Default | Meaning
163------------------ | ------- | ------- | ---------------------------
164base | number | 10 | numeric base (radix) to use, in the range 2 to 36
165allowSign | boolean | true | whether to interpret any leading `+` (positive) and `-` (negative) characters
166allowImprecise | boolean | false | whether to accept values that may have lost precision (past `MAX_SAFE_INTEGER` or below `MIN_SAFE_INTEGER`)
167allowPrefix | boolean | false | whether to interpret the prefixes `0b` (base 2), `0o` (base 8), `0t` (base 10), or `0x` (base 16)
168allowTrailing | boolean | false | whether to ignore trailing characters
169trimWhitespace | boolean | false | whether to trim any leading or trailing whitespace/line terminators
170leadingZeroIsOctal | boolean | false | whether a leading zero indicates octal
171
172Note that if `base` is unspecified, and `allowPrefix` or `leadingZeroIsOctal`
173are, then the leading characters can change the default base from 10. If `base`
174is explicitly specified and `allowPrefix` is true, then the prefix will only be
175accepted if it matches the specified base. `base` and `leadingZeroIsOctal`
176cannot be used together.
177
178**Context:** It's tricky to parse integers with JavaScript's built-in facilities
179for several reasons:
180
181- `parseInt()` and `Number()` by default allow the base to be specified in the
182 input string by a prefix (e.g., `0x` for hex).
183- `parseInt()` allows trailing nonnumeric characters.
184- `Number(str)` returns 0 when `str` is the empty string (`''`).
185- Both functions return incorrect values when the input string represents a
186 valid integer outside the range of integers that can be represented precisely.
187 Specifically, `parseInt('9007199254740993')` returns 9007199254740992.
188- Both functions always accept `-` and `+` signs before the digit.
189- Some older JavaScript engines always interpret a leading 0 as indicating
190 octal, which can be surprising when parsing input from users who expect a
191 leading zero to be insignificant.
192
193While each of these may be desirable in some contexts, there are also times when
194none of them are wanted. `parseInteger()` grants greater control over what
195input's permissible.
196
197### iso8601(date)
198
199Converts a Date object to an ISO8601 date string of the form
200"YYYY-MM-DDTHH:MM:SS.sssZ". This format is not customizable.
201
202
203### parseDateTime(str)
204
205Parses a date expressed as a string, as either a number of milliseconds since
206the epoch or any string format that Date accepts, giving preference to the
207former where these two sets overlap (e.g., strings containing small numbers).
208
209
210### hrtimeDiff(timeA, timeB)
211
212Given two hrtime readings (as from Node's `process.hrtime()`), where timeA is
213later than timeB, compute the difference and return that as an hrtime. It is
214illegal to invoke this for a pair of times where timeB is newer than timeA.
215
216### hrtimeAdd(timeA, timeB)
217
218Add two hrtime intervals (as from Node's `process.hrtime()`), returning a new
219hrtime interval array. This function does not modify either input argument.
220
221
222### hrtimeAccum(timeA, timeB)
223
224Add two hrtime intervals (as from Node's `process.hrtime()`), storing the
225result in `timeA`. This function overwrites (and returns) the first argument
226passed in.
227
228
229### hrtimeNanosec(timeA), hrtimeMicrosec(timeA), hrtimeMillisec(timeA)
230
231This suite of functions converts a hrtime interval (as from Node's
232`process.hrtime()`) into a scalar number of nanoseconds, microseconds or
233milliseconds. Results are truncated, as with `Math.floor()`.
234
235
236### validateJsonObject(schema, object)
237
238Uses JSON validation (via JSV) to validate the given object against the given
239schema. On success, returns null. On failure, *returns* (does not throw) a
240useful Error object.
241
242
243### extraProperties(object, allowed)
244
245Check an object for unexpected properties. Accepts the object to check, and an
246array of allowed property name strings. If extra properties are detected, an
247array of extra property names is returned. If no properties other than those
248in the allowed list are present on the object, the returned array will be of
249zero length.
250
251### mergeObjects(provided, overrides, defaults)
252
253Merge properties from objects "provided", "overrides", and "defaults". The
254intended use case is for functions that accept named arguments in an "args"
255object, but want to provide some default values and override other values. In
256that case, "provided" is what the caller specified, "overrides" are what the
257function wants to override, and "defaults" contains default values.
258
259The function starts with the values in "defaults", overrides them with the
260values in "provided", and then overrides those with the values in "overrides".
261For convenience, any of these objects may be falsey, in which case they will be
262ignored. The input objects are never modified, but properties in the returned
263object are not deep-copied.
264
265For example:
266
267 mergeObjects(undefined, { 'objectMode': true }, { 'highWaterMark': 0 })
268
269returns:
270
271 { 'objectMode': true, 'highWaterMark': 0 }
272
273For another example:
274
275 mergeObjects(
276 { 'highWaterMark': 16, 'objectMode': 7 }, /* from caller */
277 { 'objectMode': true }, /* overrides */
278 { 'highWaterMark': 0 }); /* default */
279
280returns:
281
282 { 'objectMode': true, 'highWaterMark': 16 }
283
284
285# Contributing
286
287See separate [contribution guidelines](CONTRIBUTING.md).
288