Skip to content

Commit

Permalink
Add double to bit encoding for OrderedCode
Browse files Browse the repository at this point in the history
  • Loading branch information
schmidt-sebastian committed Dec 15, 2021
1 parent 8d9a09e commit 8a4d444
Show file tree
Hide file tree
Showing 2 changed files with 375 additions and 0 deletions.
158 changes: 158 additions & 0 deletions packages/firestore/src/index/ordered_code_writer.ts
@@ -0,0 +1,158 @@
/**
* @license
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law | agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES | CONDITIONS OF ANY KIND, either express | implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

const LONG_SIZE = 64;
const BYTE_SIZE = 8;

/**
* The default size of the buffer. This is arbitrary, but likely larger than
* most index values so that less copies of the underlying buffer will be made.
* For large values, a single copy will made to double the buffer length.
*/
const DEFAULT_BUFFER_SIZE = 1024;

/**
* Converts a JavaScript number to a byte array (using little endian
* encoding).
*/
function doubleToLongBits(value: number): Uint8Array {
const dv = new DataView(new ArrayBuffer(8));
dv.setFloat64(0, value, false);
return new Uint8Array(dv.buffer);
}

/** Counts the number of zeros in a byte. */
export function numberOfLeadingZerosInByte(x: number): number {
if (x === 0) {
return 8;
}

let zeros = 0;
if (x >> 4 === 0) {
// Test if the first four bits are zero.
zeros += 4;
x = x << 4;
}
if (x >> 6 === 0) {
// Test if the first two (or next two) bits are zero.
zeros += 2;
x = x << 2;
}
if (x >> 7 === 0) {
// Test if the remaining bit is zero.
zeros += 1;
}
return zeros;
}

/** Counts the number of leading zeros in the given byte array. */
function numberOfLeadingZeros(bytes: Uint8Array): number {
let leadingZeros = 0;
for (let i = 0; i < bytes.length; ++i) {
const zeros = numberOfLeadingZerosInByte(bytes[i] & 0xff);
leadingZeros += zeros;
if (zeros !== 8) {
break;
}
}
return leadingZeros;
}

/**
* Returns of bytes required to store "value". Leading zero bytes are skipped.
*/
function unsignedNumLength(value: Uint8Array): number {
// This is just the number of bytes for the unsigned representation of the number.
const numBits = LONG_SIZE - numberOfLeadingZeros(value);
return Math.ceil(numBits / BYTE_SIZE);
}

/**
* OrderedCodeWriter is a minimal-allocation implementation of the writing
* behavior defined by the backend.
*
* The code is ported from its Java counterpart.
*/
export class OrderedCodeWriter {
buffer = new Uint8Array(DEFAULT_BUFFER_SIZE);
position = 0;

writeNumberAscending(val: number): void {
const value = this.toOrderedBits(val);
const len = unsignedNumLength(value);
this.ensureAvailable(1 + len);
this.buffer[this.position++] = len & 0xff; // Write the length
for (let i = value.length - len; i < value.length; ++i) {
this.buffer[this.position++] = value[i] & 0xff;
}
}

writeNumberDescending(val: number): void {
const value = this.toOrderedBits(val);
const len = unsignedNumLength(value);
this.ensureAvailable(1 + ~len);
this.buffer[this.position++] = ~(len & 0xff); // Write the length
for (let i = value.length - len; i < value.length; ++i) {
this.buffer[this.position++] = ~(value[i] & 0xff);
}
}

/**
* Encodes `val` into an encoding that has the following properties:
* The order matches the IEEE 754 floating-point comparison results with the
* following exceptions:
* -0.0 < 0.0
* all non-NaN < NaN
* NaN = NaN
*/
private toOrderedBits(val: number): Uint8Array {
const value = doubleToLongBits(val);
const isNegative = (value[0] & 0x80) !== 0;
value[0] ^= isNegative ? 0xff : 0x80;
for (let i = 1; i < value.length; ++i) {
value[i] ^= isNegative ? 0xff : 0x00;
}
return value;
}

/** Resets the buffer such that it is the same as when it was newly constructed. */
reset(): void {
this.position = 0;
}

/** Makes a copy of the encoded bytes in this buffer. */
encodedBytes(): Uint8Array {
return this.buffer.slice(0, this.position);
}

private ensureAvailable(bytes: number): void {
const minCapacity = bytes + this.position;
if (minCapacity <= this.buffer.length) {
return;
}
// Try doubling.
let newLength = this.buffer.length * 2;
// Still not big enough? Just allocate the right size.
if (newLength < minCapacity) {
newLength = minCapacity;
}
// Create the new buffer.
const newBuffer = new Uint8Array(newLength);
newBuffer.set(this.buffer); // copy old data
this.buffer = newBuffer;
}
}
217 changes: 217 additions & 0 deletions packages/firestore/test/unit/index/ordered_code_writer.test.ts
@@ -0,0 +1,217 @@
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0x00 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0x00
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect } from 'chai';

import {
numberOfLeadingZerosInByte,
OrderedCodeWriter
} from '../../../src/index/ordered_code_writer';

class ValueTestCase<T> {
constructor(
readonly val: T,
readonly ascEncoding: Uint8Array,
readonly descEncoding: Uint8Array
) {}
}

const NUMBER_TEST_CASES: Array<ValueTestCase<number>> = [
new ValueTestCase(
Number.NEGATIVE_INFINITY,
// Note: This values are taken from the Android reference implementation
new Uint8Array([0x07, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
new Uint8Array([0xf8, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
),
new ValueTestCase(
Number.MIN_SAFE_INTEGER,
new Uint8Array([0x08, 0x3c, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
new Uint8Array([0xf7, 0xc3, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
),
new ValueTestCase(
-2,
new Uint8Array([0x08, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
new Uint8Array([0xf7, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
),
new ValueTestCase(
-1,
new Uint8Array([0x08, 0x40, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
new Uint8Array([0xf7, 0xbf, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
),
new ValueTestCase(
-0.1,
new Uint8Array([0x08, 0x40, 0x46, 0x66, 0x66, 0x66, 0x66, 0x66, 0x65]),
new Uint8Array([0xf7, 0xbf, 0xb9, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9a])
),
new ValueTestCase(
0,
new Uint8Array([0x08, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
new Uint8Array([0xf7, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
),
new ValueTestCase(
Number.MIN_VALUE,
new Uint8Array([0x08, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]),
new Uint8Array([0xf7, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe])
),
new ValueTestCase(
0.1,
new Uint8Array([0x08, 0xbf, 0xb9, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9a]),
new Uint8Array([0xf7, 0x40, 0x46, 0x66, 0x66, 0x66, 0x66, 0x66, 0x65])
),
new ValueTestCase(
1,
new Uint8Array([0x08, 0xbf, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
new Uint8Array([0xf7, 0x40, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
),
new ValueTestCase(
2,
new Uint8Array([0x08, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
new Uint8Array([0xf7, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
),
new ValueTestCase(
4,
new Uint8Array([0x08, 0xc0, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
new Uint8Array([0xf7, 0x3f, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
),
new ValueTestCase(
0x08,
new Uint8Array([0x08, 0xc0, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
new Uint8Array([0xf7, 0x3f, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
),
new ValueTestCase(
0x10,
new Uint8Array([0x08, 0xc0, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
new Uint8Array([0xf7, 0x3f, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
),
new ValueTestCase(
0x20,
new Uint8Array([0x08, 0xc0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
new Uint8Array([0xf7, 0x3f, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
),
new ValueTestCase(
0x40,
new Uint8Array([0x08, 0xc0, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
new Uint8Array([0xf7, 0x3f, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
),
new ValueTestCase(
0x80,
new Uint8Array([0x08, 0xc0, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
new Uint8Array([0xf7, 0x3f, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
),
new ValueTestCase(
0xff,
new Uint8Array([0x08, 0xc0, 0x6f, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00]),
new Uint8Array([0xf7, 0x3f, 0x90, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff])
),
new ValueTestCase(
256,
new Uint8Array([0x08, 0xc0, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
new Uint8Array([0xf7, 0x3f, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
),
new ValueTestCase(
257,
new Uint8Array([0x08, 0xc0, 0x70, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00]),
new Uint8Array([0xf7, 0x3f, 0x8f, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff])
),
new ValueTestCase(
Number.MAX_SAFE_INTEGER,
new Uint8Array([0x08, 0xc3, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
new Uint8Array([0xf7, 0x3c, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
),
new ValueTestCase(
Number.POSITIVE_INFINITY,
new Uint8Array([0x08, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
new Uint8Array([0xf7, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
),
new ValueTestCase(
Number.NaN,
new Uint8Array([0x08, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
new Uint8Array([0xf7, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
)
];

describe('Ordered Code Writer', () => {
it('computes number of leading zeros', () => {
for (let i = 0; i < 0xff; ++i) {
let zeros = 0;
for (let bit = 7; bit >= 0; --bit) {
if ((i & (1 << bit)) === 0) {
++zeros;
} else {
break;
}
}
expect(numberOfLeadingZerosInByte(i)).to.equal(zeros, `for number ${i}`);
}
});

it('converts numbers to bits', () => {
for (let i = 0; i < NUMBER_TEST_CASES.length; ++i) {
const bytes = getBytes(NUMBER_TEST_CASES[i].val);
expect(bytes.asc).to.deep.equal(
NUMBER_TEST_CASES[i].ascEncoding,
'Ascending for ' + NUMBER_TEST_CASES[i].val
);
expect(bytes.desc).to.deep.equal(
NUMBER_TEST_CASES[i].descEncoding,
'Descending for ' + NUMBER_TEST_CASES[i].val
);
}
});

it('orders numbers correctly', () => {
for (let i = 0; i < NUMBER_TEST_CASES.length; ++i) {
for (let j = i; j < NUMBER_TEST_CASES.length; ++j) {
const left = NUMBER_TEST_CASES[i].val;
const leftBytes = getBytes(left);
const right = NUMBER_TEST_CASES[j].val;
const rightBytes = getBytes(right);
expect(compare(leftBytes.asc, rightBytes.asc)).to.equal(
i === j ? 0 : -1,
`Ascending order: ${left} vs ${right}`
);
expect(compare(leftBytes.desc, rightBytes.desc)).to.equal(
i === j ? 0 : 1,
`Descending order: ${left} vs ${right}`
);
}
}
});
});

function compare(left: Uint8Array, right: Uint8Array): number {
for (let i = 0; i < Math.min(left.length, right.length); ++i) {
if (left[i] < right[i]) {
return -1;
}
if (left[i] > right[i]) {
return 1;
}
}
return left.length - right.length;
}

function getBytes(val: unknown): { asc: Uint8Array; desc: Uint8Array } {
const ascWriter = new OrderedCodeWriter();
const descWriter = new OrderedCodeWriter();
if (typeof val === 'number') {
ascWriter.writeNumberAscending(val);
descWriter.writeNumberDescending(val);
} else {
throw new Error('Encoding not yet supported for ' + val);
}
return { asc: ascWriter.encodedBytes(), desc: descWriter.encodedBytes() };
}

0 comments on commit 8a4d444

Please sign in to comment.