Skip to main content

4 posts tagged with "javascript"

View All Tags

· One min read
Peter Johnson

Javascript currently lacks an IEEE 754 floating-point remainder function.

You'll find no shortage of developers complaining about how broken the Javascript Modulo operator is online.

There's actually a TC39 proposal to improve things.

In the meantime you might like to use this function which is equivalent to the math.Remainder function from the Go stdlib:

/** Computes the IEEE 754 floating-point remainder of x / y. */
const remainder = (x, y) => {
if (isNaN(x) || isNaN(y) || !isFinite(x) || y === 0) return NaN

const quotient = x / y
let n = Math.round(quotient)

// When quotient is exactly halfway between two integers, round to the nearest even integer
if (Math.abs(quotient - n) === 0.5) n = 2 * Math.round(quotient / 2)

const rem = x - n * y
return !rem ? Math.sign(x) * 0 : rem
}
-5 % 3
-2

remainder(-5, 3)
1

· 2 min read
Peter Johnson

Javascript Generators are a memory-efficent way of iterating over arbitrary data.

For instance you can store integers as deltas and retrieve values & offsets with an iterator:

function* deltaIterator (deltas) {
let [v, o] = [0, 0]
for (const d of deltas) {
yield [v += d, o++]
}
}
for (const [v, o] of deltaIterator([1, 3, 12, 2, 72])) {
console.log(o, v)
}

0 1
1 4
2 16
3 18
4 90

Set Operations

I wasn't able to find an example online of set operations (intersection, union) using generators so I'm posting an example below:

Intersection

The intersection generator takes two iterators and yields only values which are members of both sets:

function *intersection (A, B) {
let a = A.next()
let b = B.next()
while (!a.done && !b.done) {
if (a.value[0] < b.value[0]) { a = A.next(); continue }
if (a.value[0] > b.value[0]) { b = B.next(); continue }
yield a.value[0]
a = A.next()
}
}
const A = deltaIterator([1, 3, 12, 2, 72])
const B = deltaIterator([1, 12, 2, 72, 3])

for (const v of intersection(A, B)) {
console.log(v)
}

1
90

Union

The union generator takes two iterators and yields unique values which are members of either set:

function *union (A, B) {
let a = A.next()
let b = B.next()
let v = 0
while (!a.done || !b.done) {
if (!a.done && a.value[0] <= v) { a = A.next(); continue }
if (!b.done && b.value[0] <= v) { b = B.next(); continue }
yield v = Math.min(
a.value ? a.value[0] : Infinity,
b.value ? b.value[0] : Infinity
)
}
}
const A = deltaIterator([1, 3, 12, 2, 72])
const B = deltaIterator([1, 12, 2, 72, 3, 1, 1, 1])

for (const v of union(A, B)) {
console.log(v)
}

1
4
13
15
16
18
87
90
91
92
93

· 2 min read
Peter Johnson

Object keys in a Javascript maintain insertion order.

For some crypto tasks, such as hashing and signing, it's desirable to have a normalized view of the data.

Fortunately it's easy enough to recursively sort object keys using just a few lines of code:

function replacer (key, value) {
switch (Object.prototype.toString.call(value)) {

// sort object keys lexicographically
case '[object Object]':
return Object.fromEntries(Object.entries(value).sort())

default:
return value
}
}

Example

You can use it as you normally would with JSON.stringify():

const example = {
"isbn": "123-456-222",
"author": {
"lastname": "Doe",
"firstname": "Jane"
},
"editor": {
"lastname": "Smith",
"firstname": "Jane"
},
"title": "The Ultimate Database Study Guide",
"category": [
"Non-Fiction",
"Technology"
]
}

> JSON.stringify(example, replacer, 2)

{
"author": {
"firstname": "Jane",
"lastname": "Doe"
},
"category": [
"Non-Fiction",
"Technology"
],
"editor": {
"firstname": "Jane",
"lastname": "Smith"
},
"isbn": "123-456-222",
"title": "The Ultimate Database Study Guide"
}

Additional types

It's also possible to extend this method to other types:

function replacer (key, value) {
switch (Object.prototype.toString.call(value)) {

// sort object keys lexicographically
case '[object Object]':
return Object.fromEntries(Object.entries(value).sort())

// Unicode Normalization Form KC
case '[object String]':
return value.normalize('NFKC')

// round floats to 7 decimal places of precision
case '[object Number]':
return value % 1 === 0
? value
: Math.round(parseFloat(value) * 1e7) / 1e7

default:
return value
}
}

· 2 min read
Peter Johnson

One little used option of JSON.stringify(value, replacer, space) is the second argument replacer, which i've never seen set to anything except null.

As it turns out, that's for a very good reason. The replacer argument can either be provided as an Array or a Function, and both implementations have poor support for nested JSON.

warning

Using either syntax results in considerably more bug than feature.

Saving Grace

However! in the footnotes of the documentation we find a clue to saving this feature from the 🗑️ of history:

The object in which the key was found is provided as the replacer's this context.

In other words, it calls (key, value) for the current JSON element, but also binds this to the parent element 🤔

Get the JSON Path within replacer

With this in mind we can create a decorator function which tracks the JSON Path for each key as we traverse the object hierarchy:

function replacerWithPath (fn) {
const paths = new Map()
return function (key, value) {
let path = paths.get(this) || '$'
if (key) path += Array.isArray(this) ? `[${key}]` : `.${key}`
const v = fn(key, value, path)
if (v === Object(v)) paths.set(v, path)
return v
}
}

Now our replacer function will receive a third argument with the full path:

function replacer (key, value, path) {
console.log(path, key)
return value
}

Example

You can use it as you normally would with JSON.stringify():

const example = {
"isbn": "123-456-222",
"author": {
"lastname": "Doe",
"firstname": "Jane"
},
"editor": {
"lastname": "Smith",
"firstname": "Jane"
},
"title": "The Ultimate Database Study Guide",
"category": [
"Non-Fiction",
"Technology"
]
}

> JSON.stringify(example, replacerWithPath(replacer), 2)

$
$.isbn isbn
$.author author
$.author.lastname lastname
$.author.firstname firstname
$.editor editor
$.editor.lastname lastname
$.editor.firstname firstname
$.title title
$.category category
$.category[0] 0
$.category[1] 1