Last active
May 6, 2025 10:17
-
-
Save VictorQueiroz/071599802a4e00e591af4e2a41ced5ac to your computer and use it in GitHub Desktop.
Converts a typed array to a different typed array type.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import test, { describe } from "node:test"; | |
| import quantizeToFloat32 from "../../quantizeToFloat32"; | |
| import castTypedArray from "./castTypedArray"; | |
| describe("asTypedArray", async () => { | |
| const { expect } = await import("chai"); | |
| test("it should convert Float32Array to Uint8Array without data loss", () => { | |
| const expected = new Array(48) | |
| .fill(0.0) | |
| .map(() => quantizeToFloat32(Math.random())); | |
| const initialInput = new Float32Array(expected); | |
| // Cast from Float32Array to Uint8Array | |
| const output = castTypedArray(initialInput, Uint8Array); | |
| // Cast from Uint8Array to Float32Array | |
| const expectedOutput = castTypedArray(output, Float32Array); | |
| expect(output).not.to.be.deep.equal(initialInput); | |
| expect(initialInput).to.be.deep.equal(expectedOutput); | |
| expect(expectedOutput).to.be.deep.equal(new Float32Array(expected)); | |
| expect(initialInput).to.be.deep.equal(new Float32Array(expected)); | |
| }); | |
| test('it should return the same reference if the input and output types are the same instance', () => { | |
| const input = new Float32Array([0.125, 0.25, 0.5, 0.75, 1]); | |
| const output = castTypedArray(input, Float32Array); | |
| expect(input).to.be.equal(output); | |
| }); | |
| describe("Type-checking", () => { | |
| test("it should return an instance of the typed array constructor given as the second argument", async () => { | |
| const testA: Uint8Array = castTypedArray( | |
| new Float32Array([]), | |
| Uint8Array | |
| ); | |
| // @ts-expect-error Make sure we are returning the correct typed array | |
| const testB: Float32Array = castTypedArray(testA, Uint8Array); | |
| // @ts-expect-error Make sure we are returning TypedArray<ArrayBuffer> | |
| const testC: Uint8Array<SharedArrayBuffer> = castTypedArray( | |
| testA, | |
| Uint8Array | |
| ); | |
| }); | |
| test("it should output an instance type of the second argument", () => { | |
| const output = castTypedArray( | |
| new Float32Array([0.125, 0.25, 0.5, 0.75, 1]), | |
| Uint8Array | |
| ); | |
| let output2: Float32Array; | |
| // @ts-expect-error Make sure we are returning the correct type in TypeScript | |
| output2 = output; | |
| // Remove `error TS6133: 'output2' is declared but its value is never read.` | |
| output2; | |
| }); | |
| }); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export type TypedArrayConstructor = | |
| | Uint16ArrayConstructor | |
| | Uint8ArrayConstructor | |
| | Uint32ArrayConstructor | |
| | Int16ArrayConstructor | |
| | Int8ArrayConstructor | |
| | Int32ArrayConstructor | |
| | Float64ArrayConstructor | |
| | Float32ArrayConstructor; | |
| export type TypedArray<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike> = | |
| | Uint16Array<TArrayBuffer> | |
| | Uint8Array<TArrayBuffer> | |
| | Uint32Array<TArrayBuffer> | |
| | Int16Array<TArrayBuffer> | |
| | Int8Array<TArrayBuffer> | |
| | Int32Array<TArrayBuffer> | |
| | Float64Array<TArrayBuffer> | |
| | Float32Array<TArrayBuffer>; | |
| // Use a custom class for the error, to avoid `error instanceof Error` to return false when it is an extension of an error. | |
| // Do some research on TypeScript to understand the reason of this approach. | |
| export class ErrorCastTypedArray extends Error { | |
| public constructor(public readonly what: string) { | |
| super(what); | |
| } | |
| } | |
| export interface ITypedArrayConstructor< | |
| T extends TypedArray, | |
| TArrayBuffer extends ArrayBufferLike = ArrayBufferLike | |
| > { | |
| new(buffer: TArrayBuffer, byteOffset?: number, length?: number): T; | |
| new(elements: Iterable<number>): T; | |
| readonly BYTES_PER_ELEMENT: number; | |
| } | |
| /** | |
| * Casts a typed array to another typed array. | |
| * | |
| * Throws an exception if the source buffer is not a multiple of the element size. | |
| * | |
| * @param input The typed array to cast. | |
| * @param TypedArray The typed array constructor to cast to. | |
| * @returns The casted typed array. | |
| */ | |
| export default function castTypedArray< | |
| T extends TypedArray, | |
| U extends TypedArray | |
| >(input: T, TypedArray: ITypedArrayConstructor<U>): U { | |
| // If the `input` is an instance of `TypedArray` constructor, | |
| // just return the same reference back. | |
| if (input instanceof TypedArray) { | |
| return input; | |
| } | |
| /** | |
| * If the typed array is empty, return an empty typed array | |
| */ | |
| if (input.byteLength < 1) { | |
| return new TypedArray([]); | |
| } | |
| /** | |
| * Count of elements in the new typed array. This is needed since | |
| * there are certain typed arrays that are not divisible by the element size, | |
| * such as `Float32Array` and `Float64Array` classes, which has `BYTES_PER_ELEMENT` respectively | |
| * equal to 4 bytes, and 8 bytes. | |
| */ | |
| const elementCount = input.byteLength / TypedArray.BYTES_PER_ELEMENT; | |
| const isValid = | |
| input.byteLength % TypedArray.BYTES_PER_ELEMENT === 0 && | |
| elementCount % TypedArray.BYTES_PER_ELEMENT === 0; | |
| if (!isValid) { | |
| throw new ErrorCastTypedArray( | |
| [ | |
| `The buffer has ${input.byteLength}, but the typed array contains ${TypedArray.BYTES_PER_ELEMENT} bytes per element.`, | |
| `buffer.byteLength / TypedArray.BYTES_PER_ELEMENT = ${input.byteLength / TypedArray.BYTES_PER_ELEMENT}`, | |
| `buffer.byteLength % TypedArray.BYTES_PER_ELEMENT = ${input.byteLength % TypedArray.BYTES_PER_ELEMENT}` | |
| ] | |
| .map((s, i) => (i === 0 ? s : `\t${s}`)) | |
| .join("\n") | |
| ); | |
| } | |
| const bufferClone = new ArrayBuffer(input.byteLength); | |
| const viewClone = new TypedArray(bufferClone, 0, elementCount); | |
| // Transfer the data to the clone | |
| viewClone.set(new TypedArray(input.buffer, input.byteOffset, elementCount)); | |
| // TODO: Add support for TypedArray<SharedArrayBuffer> | |
| return viewClone; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Quantize a number to a 32-bit float. | |
| * | |
| * @param originalNumber - The number to quantize. If not provided, a random | |
| * number in [-100, 100) is chosen for demonstration. | |
| * @returns The closest representable 32-bit float value. | |
| * | |
| * This function creates a temporary 32-bit float buffer, assigns the input | |
| * number to it, and reads the value back out. This process performs the | |
| * conversion/quantization to 32-bit precision according to IEEE 754 rounding | |
| * rules. The returned value is still a 64-bit JavaScript 'number' type, but | |
| * its value corresponds precisely to a 32-bit float representation. | |
| */ | |
| export default function quantizeToFloat32(originalNumber: number): number { | |
| // Create a temporary buffer (4 bytes for one float32) | |
| const buffer = new ArrayBuffer(4); | |
| // Create a Float32 view on the buffer | |
| const view = new Float32Array(buffer); | |
| // Determine the number to quantize | |
| // If no number is provided, generate a sample random one for demonstration. | |
| const numToQuantize = | |
| originalNumber !== undefined ? originalNumber : Math.random() * 200 - 100; // Example: random number in [-100, 100) | |
| // Assign the number to the Float32Array view. | |
| // This performs the crucial conversion/quantization to 32-bit precision | |
| // according to IEEE 754 rounding rules. | |
| view[0] = numToQuantize; | |
| // Read the value back from the view. | |
| // This value is now guaranteed to be exactly representable as a 32-bit float. | |
| // It's still stored in a 64-bit JavaScript 'number' type, but its value | |
| // corresponds precisely to a 32-bit float representation. | |
| const quantizedValue = view[0]; | |
| return quantizedValue; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment