diff --git a/packages/smithy-client/src/parse-utils.spec.ts b/packages/smithy-client/src/parse-utils.spec.ts index f5fb55d8af872..78ec4bc3003ca 100644 --- a/packages/smithy-client/src/parse-utils.spec.ts +++ b/packages/smithy-client/src/parse-utils.spec.ts @@ -1,4 +1,19 @@ -import { expectInt, limitedParseFloat, parseBoolean, strictParseFloat, strictParseInt } from "./parse-utils"; +import { + expectByte, + expectFloat32, + expectInt32, + expectLong, + expectShort, + limitedParseDouble, + limitedParseFloat32, + parseBoolean, + strictParseByte, + strictParseDouble, + strictParseFloat32, + strictParseInt32, + strictParseLong, + strictParseShort, +} from "./parse-utils"; import { expectBoolean, expectNumber, expectString } from "./parse-utils"; describe("parseBoolean", () => { @@ -83,18 +98,151 @@ describe("expectNumber", () => { }); }); -describe("expectInt", () => { - it("accepts integers", () => { - expect(expectInt(1)).toEqual(1); +describe("expectFloat32", () => { + describe("accepts numbers", () => { + it.each([ + 1, + 1.1, + Infinity, + -Infinity, + // Smallest positive subnormal number + 2 ** -149, + // Largest subnormal number + 2 ** -126 * (1 - 2 ** -23), + // Smallest positive normal number + 2 ** -126, + // Largest normal number + 2 ** 127 * (2 - 2 ** -23), + // Largest number less than one + 1 - 2 ** -24, + // Smallest number larger than one + 1 + 2 ** -23, + ])("accepts %s", (value) => { + expect(expectNumber(value)).toEqual(value); + }); + }); + + it.each([null, undefined])("accepts %s", (value) => { + expect(expectNumber(value)).toEqual(undefined); + }); + + describe("rejects non-numbers", () => { + it.each(["1", "1.1", "Infinity", "-Infinity", "NaN", true, false, [], {}])("rejects %s", (value) => { + expect(() => expectNumber(value)).toThrowError(); + }); + }); + + describe("rejects doubles", () => { + it.each([2 ** 128, -(2 ** 128)])("rejects %s", (value) => { + expect(() => expectFloat32(value)).toThrowError(); + }); + }); +}); + +describe("expectLong", () => { + describe("accepts 64-bit integers", () => { + it.each([1, 2 ** 63 - 1, -(2 ** 63), 2 ** 31 - 1, -(2 ** 31), 2 ** 15 - 1, -(2 ** 15), 127, -128])( + "accepts %s", + (value) => { + expect(expectLong(value)).toEqual(value); + } + ); }); it.each([null, undefined])("accepts %s", (value) => { - expect(expectInt(value)).toEqual(undefined); + expect(expectLong(value)).toEqual(undefined); }); describe("rejects non-integers", () => { it.each([1.1, "1", "1.1", NaN, true, [], {}])("rejects %s", (value) => { - expect(() => expectInt(value)).toThrowError(); + expect(() => expectLong(value)).toThrowError(); + }); + }); +}); + +describe("expectInt32", () => { + describe("accepts 32-bit integers", () => { + it.each([1, 2 ** 31 - 1, -(2 ** 31), 2 ** 15 - 1, -(2 ** 15), 127, -128])("accepts %s", (value) => { + expect(expectInt32(value)).toEqual(value); + }); + }); + + it.each([null, undefined])("accepts %s", (value) => { + expect(expectInt32(value)).toEqual(undefined); + }); + + describe("rejects non-integers", () => { + it.each([1.1, "1", "1.1", NaN, true, [], {}, 2 ** 63 - 1, -(2 ** 63 + 1), 2 ** 31, -(2 ** 31 + 1)])( + "rejects %s", + (value) => { + expect(() => expectInt32(value)).toThrowError(); + } + ); + }); +}); + +describe("expectShort", () => { + describe("accepts 16-bit integers", () => { + it.each([1, 2 ** 15 - 1, -(2 ** 15), 127, -128])("accepts %s", (value) => { + expect(expectShort(value)).toEqual(value); + }); + }); + + it.each([null, undefined])("accepts %s", (value) => { + expect(expectShort(value)).toEqual(undefined); + }); + + describe("rejects non-integers", () => { + it.each([ + 1.1, + "1", + "1.1", + NaN, + true, + [], + {}, + 2 ** 63 - 1, + -(2 ** 63 + 1), + 2 ** 31 - 1, + -(2 ** 31 + 1), + 2 ** 15, + -(2 ** 15 + 1), + ])("rejects %s", (value) => { + expect(() => expectShort(value)).toThrowError(); + }); + }); +}); + +describe("expectByte", () => { + describe("accepts 8-bit integers", () => { + it.each([1, 127, -128])("accepts %s", (value) => { + expect(expectByte(value)).toEqual(value); + }); + }); + + it.each([null, undefined])("accepts %s", (value) => { + expect(expectByte(value)).toEqual(undefined); + }); + + describe("rejects non-integers", () => { + it.each([ + 1.1, + "1", + "1.1", + NaN, + true, + [], + {}, + 2 ** 63 - 1, + -(2 ** 63 + 1), + 2 ** 31 - 1, + -(2 ** 31 + 1), + 2 ** 15 - 1, + -(2 ** 15 + 1), + 128, + -129, + ])("rejects %s", (value) => { + expect(() => expectByte(value)).toThrowError(); }); }); }); @@ -115,77 +263,307 @@ describe("expectString", () => { }); }); -describe("strictParseFloat", () => { +describe("strictParseDouble", () => { describe("accepts non-numeric floats as strings", () => { - expect(strictParseFloat("Infinity")).toEqual(Infinity); - expect(strictParseFloat("-Infinity")).toEqual(-Infinity); - expect(strictParseFloat("NaN")).toEqual(NaN); + expect(strictParseDouble("Infinity")).toEqual(Infinity); + expect(strictParseDouble("-Infinity")).toEqual(-Infinity); + expect(strictParseDouble("NaN")).toEqual(NaN); }); it("rejects implicit NaN", () => { - expect(() => strictParseFloat("foo")).toThrowError(); + expect(() => strictParseDouble("foo")).toThrowError(); }); it("accepts numeric strings", () => { - expect(strictParseFloat("1")).toEqual(1); - expect(strictParseFloat("1.1")).toEqual(1.1); + expect(strictParseDouble("1")).toEqual(1); + expect(strictParseDouble("1.1")).toEqual(1.1); }); describe("accepts numbers", () => { it.each([1, 1.1, Infinity, -Infinity, NaN])("accepts %s", (value) => { - expect(strictParseFloat(value)).toEqual(value); + expect(strictParseDouble(value)).toEqual(value); }); }); it.each([null, undefined])("accepts %s", (value) => { - expect(strictParseFloat(value)).toEqual(undefined); + expect(strictParseDouble(value)).toEqual(undefined); }); }); -describe("limitedParseFloat", () => { +describe("strictParseFloat32", () => { + describe("accepts non-numeric floats as strings", () => { + expect(strictParseFloat32("Infinity")).toEqual(Infinity); + expect(strictParseFloat32("-Infinity")).toEqual(-Infinity); + expect(strictParseFloat32("NaN")).toEqual(NaN); + }); + + it("rejects implicit NaN", () => { + expect(() => strictParseFloat32("foo")).toThrowError(); + }); + + describe("rejects doubles", () => { + it.each([2 ** 128, -(2 ** 128)])("rejects %s", (value) => { + expect(() => strictParseFloat32(value)).toThrowError(); + }); + }); + + it("accepts numeric strings", () => { + expect(strictParseFloat32("1")).toEqual(1); + expect(strictParseFloat32("1.1")).toEqual(1.1); + }); + + describe("accepts numbers", () => { + it.each([1, 1.1, Infinity, -Infinity, NaN])("accepts %s", (value) => { + expect(strictParseFloat32(value)).toEqual(value); + }); + }); + + it.each([null, undefined])("accepts %s", (value) => { + expect(strictParseFloat32(value)).toEqual(undefined); + }); +}); + +describe("limitedParseDouble", () => { it("accepts non-numeric floats as strings", () => { - expect(limitedParseFloat("Infinity")).toEqual(Infinity); - expect(limitedParseFloat("-Infinity")).toEqual(-Infinity); - expect(limitedParseFloat("NaN")).toEqual(NaN); + expect(limitedParseDouble("Infinity")).toEqual(Infinity); + expect(limitedParseDouble("-Infinity")).toEqual(-Infinity); + expect(limitedParseDouble("NaN")).toEqual(NaN); }); it("rejects implicit NaN", () => { - expect(() => limitedParseFloat("foo")).toThrowError(); + expect(() => limitedParseDouble("foo")).toThrowError(); }); describe("rejects numeric strings", () => { it.each(["1", "1.1"])("rejects %s", (value) => { - expect(() => limitedParseFloat(value)).toThrowError(); + expect(() => limitedParseDouble(value)).toThrowError(); }); }); describe("accepts numbers", () => { - it.each([1, 1.1, Infinity, -Infinity, NaN])("accepts %s", (value) => { - expect(limitedParseFloat(value)).toEqual(value); + it.each([ + 1, + 1.1, + Infinity, + -Infinity, + NaN, + // Smallest positive subnormal number + 2 ** -1074, + // Largest subnormal number + 2 ** -1022 * (1 - 2 ** -52), + // Smallest positive normal number + 2 ** -1022, + // Largest number + 2 ** 1023 * (1 + (1 - 2 ** -52)), + // Largest number less than one + 1 - 2 ** -53, + // Smallest number larger than one + 1 + 2 ** -52, + ])("accepts %s", (value) => { + expect(limitedParseDouble(value)).toEqual(value); + }); + }); + + it.each([null, undefined])("accepts %s", (value) => { + expect(limitedParseDouble(value)).toEqual(undefined); + }); +}); + +describe("limitedParseFloat32", () => { + it("accepts non-numeric floats as strings", () => { + expect(limitedParseFloat32("Infinity")).toEqual(Infinity); + expect(limitedParseFloat32("-Infinity")).toEqual(-Infinity); + expect(limitedParseFloat32("NaN")).toEqual(NaN); + }); + + it("rejects implicit NaN", () => { + expect(() => limitedParseFloat32("foo")).toThrowError(); + }); + + describe("rejects numeric strings", () => { + it.each(["1", "1.1"])("rejects %s", (value) => { + expect(() => limitedParseFloat32(value)).toThrowError(); + }); + }); + + describe("accepts numbers", () => { + it.each([ + 1, + 1.1, + Infinity, + -Infinity, + NaN, + // Smallest positive subnormal number + 2 ** -149, + // Largest subnormal number + 2 ** -126 * (1 - 2 ** -23), + // Smallest positive normal number + 2 ** -126, + // Largest normal number + 2 ** 127 * (2 - 2 ** -23), + // Largest number less than one + 1 - 2 ** -24, + // Smallest number larger than one + 1 + 2 ** -23, + ])("accepts %s", (value) => { + expect(limitedParseFloat32(value)).toEqual(value); + }); + }); + + describe("rejects doubles", () => { + it.each([2 ** 128, -(2 ** 128)])("rejects %s", (value) => { + expect(() => limitedParseFloat32(value)).toThrowError(); }); }); it.each([null, undefined])("accepts %s", (value) => { - expect(limitedParseFloat(value)).toEqual(undefined); + expect(limitedParseFloat32(value)).toEqual(undefined); }); }); -describe("strictParseInt", () => { - it("accepts integers", () => { - expect(strictParseInt(1)).toEqual(1); - expect(strictParseInt("1")).toEqual(1); +describe("strictParseLong", () => { + describe("accepts integers", () => { + describe("accepts 64-bit integers", () => { + it.each([1, 2 ** 63 - 1, -(2 ** 63), 2 ** 31 - 1, -(2 ** 31), 2 ** 15 - 1, -(2 ** 15), 127, -128])( + "accepts %s", + (value) => { + expect(strictParseLong(value)).toEqual(value); + } + ); + }); + expect(strictParseLong("1")).toEqual(1); }); it.each([null, undefined])("accepts %s", (value) => { - expect(strictParseInt(value)).toEqual(undefined); + expect(strictParseLong(value)).toEqual(undefined); }); describe("rejects non-integers", () => { it.each([1.1, "1.1", "NaN", "Infinity", "-Infinity", NaN, Infinity, -Infinity, true, false, [], {}])( "rejects %s", (value) => { - expect(() => strictParseInt(value as any)).toThrowError(); + expect(() => strictParseLong(value as any)).toThrowError(); } ); }); }); + +describe("strictParseInt32", () => { + describe("accepts integers", () => { + describe("accepts 32-bit integers", () => { + it.each([1, 2 ** 31 - 1, -(2 ** 31), 2 ** 15 - 1, -(2 ** 15), 127, -128])("accepts %s", (value) => { + expect(strictParseInt32(value)).toEqual(value); + }); + }); + expect(strictParseInt32("1")).toEqual(1); + }); + + it.each([null, undefined])("accepts %s", (value) => { + expect(strictParseInt32(value)).toEqual(undefined); + }); + + describe("rejects non-integers", () => { + it.each([ + 1.1, + "1.1", + "NaN", + "Infinity", + "-Infinity", + NaN, + Infinity, + -Infinity, + true, + false, + [], + {}, + 2 ** 63 - 1, + -(2 ** 63 + 1), + 2 ** 31, + -(2 ** 31 + 1), + ])("rejects %s", (value) => { + expect(() => strictParseInt32(value as any)).toThrowError(); + }); + }); +}); + +describe("strictParseShort", () => { + describe("accepts integers", () => { + describe("accepts 16-bit integers", () => { + it.each([1, 2 ** 15 - 1, -(2 ** 15), 127, -128])("accepts %s", (value) => { + expect(strictParseShort(value)).toEqual(value); + }); + }); + expect(strictParseShort("1")).toEqual(1); + }); + + it.each([null, undefined])("accepts %s", (value) => { + expect(strictParseShort(value)).toEqual(undefined); + }); + + describe("rejects non-integers", () => { + it.each([ + 1.1, + "1.1", + "NaN", + "Infinity", + "-Infinity", + NaN, + Infinity, + -Infinity, + true, + false, + [], + {}, + 2 ** 63 - 1, + -(2 ** 63 + 1), + 2 ** 31 - 1, + -(2 ** 31 + 1), + 2 ** 15, + -(2 ** 15 + 1), + ])("rejects %s", (value) => { + expect(() => strictParseShort(value as any)).toThrowError(); + }); + }); +}); + +describe("strictParseByte", () => { + describe("accepts integers", () => { + describe("accepts 8-bit integers", () => { + it.each([1, 127, -128])("accepts %s", (value) => { + expect(strictParseByte(value)).toEqual(value); + }); + }); + expect(strictParseByte("1")).toEqual(1); + }); + + it.each([null, undefined])("accepts %s", (value) => { + expect(strictParseByte(value)).toEqual(undefined); + }); + + describe("rejects non-integers", () => { + it.each([ + 1.1, + "1.1", + "NaN", + "Infinity", + "-Infinity", + NaN, + Infinity, + -Infinity, + true, + false, + [], + {}, + 2 ** 63 - 1, + -(2 ** 63 + 1), + 2 ** 31 - 1, + -(2 ** 31 + 1), + 2 ** 15, + -(2 ** 15 + 1), + 128, + -129, + ])("rejects %s", (value) => { + expect(() => strictParseByte(value as any)).toThrowError(); + }); + }); +}); diff --git a/packages/smithy-client/src/parse-utils.ts b/packages/smithy-client/src/parse-utils.ts index 0e80f48aba4b5..c4eda2310a668 100644 --- a/packages/smithy-client/src/parse-utils.ts +++ b/packages/smithy-client/src/parse-utils.ts @@ -49,6 +49,57 @@ export const expectNumber = (value: any): number | undefined => { throw new TypeError(`Expected number, got ${typeof value}`); }; +const MAX_FLOAT = Math.ceil(2 ** 127 * (2 - 2 ** -23)); + +/** + * Asserts a value is a 32-bit float and returns it. + * + * @param value A value that is expected to be a 32-bit float. + * @returns The value if it's a float, undefined if it's null/undefined, + * otherwise an error is thrown. + */ +export const expectFloat32 = (value: any): number | undefined => { + const expected = expectNumber(value); + if (expected !== undefined && !Number.isNaN(expected) && expected !== Infinity && expected !== -Infinity) { + // IEEE-754 is an imperfect representation for floats. Consider the simple + // value `0.1`. The representation in a 32-bit float would look like: + // + // 0 01111011 10011001100110011001101 + // Actual value: 0.100000001490116119384765625 + // + // Note the repeasting pattern of `1001` in the fraction part. The 64-bit + // representation is similar: + // + // 0 01111111011 1001100110011001100110011001100110011001100110011010 + // Actual value: 0.100000000000000005551115123126 + // + // So even for what we consider simple numbers, the representation differs + // between the two formats. And it's non-obvious how one might look at the + // 64-bit value (which is how JS represents numbers) and determine if it + // can be represented reasonably in the 32-bit form. Primarily because you + // can't know whether the intent was to represent `0.1` or the actual + // value in memory. But even if you have both the decimal value and the + // double value, that still doesn't communicate the intended precision. + // + // So rather than attempting to divine the intent of the caller, we instead + // do some simple bounds checking to make sure the value is passingly + // representable in a 32-bit float. It's not perfect, but it's good enough + // and perfect, even if possible to achieve, would likely be too costly to + // be worth it. + // + // The maximum value of a 32-bit float. Since the 64-bit representation + // could be more or less, we just round it up to the nearest whole number. + // This further reduces our ability to be certain of the value, but it's + // an acceptable tradeoff. + + // Compare against the absolute value to simplify things. + if (Math.abs(expected) > MAX_FLOAT) { + throw new TypeError(`Expected 32-bit float, got ${value}`); + } + } + return expected; +}; + /** * Asserts a value is an integer and returns it. * @@ -56,7 +107,7 @@ export const expectNumber = (value: any): number | undefined => { * @returns The value if it's an integer, undefined if it's null/undefined, * otherwise an error is thrown. */ -export const expectInt = (value: any): number | undefined => { +export const expectLong = (value: any): number | undefined => { if (value === null || value === undefined) { return undefined; } @@ -66,6 +117,56 @@ export const expectInt = (value: any): number | undefined => { throw new TypeError(`Expected integer, got ${typeof value}`); }; +/** + * Asserts a value is a 32-bit integer and returns it. + * + * @param value A value that is expected to be an integer. + * @returns The value if it's an integer, undefined if it's null/undefined, + * otherwise an error is thrown. + */ +export const expectInt32 = (value: any): number | undefined => { + const expected = expectLong(value); + if (expected !== undefined && Int32Array.of(expected)[0] !== expected) { + throw new TypeError(`Expected 32-bit integer, got ${value}`); + } + return expected; +}; + +/** + * Asserts a value is a 16-bit integer and returns it. + * + * @param value A value that is expected to be an integer. + * @returns The value if it's an integer, undefined if it's null/undefined, + * otherwise an error is thrown. + */ +export const expectShort = (value: any): number | undefined => { + const expected = expectLong(value); + if (expected !== undefined && Int16Array.of(expected)[0] !== expected) { + throw new TypeError(`Expected 16-bit integer, got ${value}`); + } + return expected; +}; + +/** + * Asserts a value is an 8-bit integer and returns it. + * + * @param value A value that is expected to be an integer. + * @returns The value if it's an integer, undefined if it's null/undefined, + * otherwise an error is thrown. + */ +export const expectByte = (value: any): number | undefined => { + const expected = expectLong(value); + if (expected !== undefined && Int8Array.of(expected)[0] !== expected) { + throw new TypeError(`Expected 8-bit integer, got ${value}`); + } + return expected; +}; + +/** + * @deprecated Use expectLong + */ +export const expectInt = expectLong; + /** * Asserts a value is a string and returns it. * @@ -84,21 +185,20 @@ export const expectString = (value: any): string | undefined => { }; /** - * Parses a value into a float. If the value is null or undefined, undefined + * Parses a value into a double. If the value is null or undefined, undefined * will be returned. If the value is a string, it will be parsed by the standard * parseFloat with one exception: NaN may only be explicitly set as the string * "NaN", any implicit Nan values will result in an error being thrown. If any * other type is provided, an exception will be thrown. * - * @param value A number or string representation of a float. + * @param value A number or string representation of a double. * @returns The value as a number, or undefined if it's null/undefined. */ -export const strictParseFloat = (value: string | number): number | undefined => { +export const strictParseDouble = (value: string | number): number | undefined => { if (value === "NaN") { return NaN; } if (typeof value == "string") { - // TODO: handle underflow / overflow explicitly const parsed: number = parseFloat(value); if (Number.isNaN(parsed)) { throw new TypeError(`Expected real number, got implicit NaN`); @@ -108,6 +208,35 @@ export const strictParseFloat = (value: string | number): number | undefined => return expectNumber(value); }; +/** + * @deprecated Use strictParseDouble + */ +export const strictParseFloat = strictParseDouble; + +/** + * Parses a value into a float. If the value is null or undefined, undefined + * will be returned. If the value is a string, it will be parsed by the standard + * parseFloat with one exception: NaN may only be explicitly set as the string + * "NaN", any implicit Nan values will result in an error being thrown. If any + * other type is provided, an exception will be thrown. + * + * @param value A number or string representation of a float. + * @returns The value as a number, or undefined if it's null/undefined. + */ +export const strictParseFloat32 = (value: string | number): number | undefined => { + if (value === "NaN") { + return NaN; + } + if (typeof value == "string") { + const parsed: number = parseFloat(value); + if (Number.isNaN(parsed)) { + throw new TypeError(`Expected real number, got implicit NaN`); + } + return expectFloat32(parsed); + } + return expectFloat32(value); +}; + /** * Asserts a value is a number and returns it. If the value is a string * representation of a non-numeric number type (NaN, Infinity, -Infinity), @@ -118,7 +247,7 @@ export const strictParseFloat = (value: string | number): number | undefined => * @param value A number or string representation of a non-numeric float. * @returns The value as a number, or undefined if it's null/undefined. */ -export const limitedParseFloat = (value: string | number): number | undefined => { +export const limitedParseDouble = (value: string | number): number | undefined => { if (typeof value == "string") { switch (value) { case "NaN": @@ -136,9 +265,40 @@ export const limitedParseFloat = (value: string | number): number | undefined => }; /** - * @deprecated Use limitedParseFloat or strictParseFloat + * @deprecated Use limitedParseDouble + */ +export const handleFloat = limitedParseDouble; + +/** + * @deprecated Use limitedParseDouble */ -export const handleFloat = limitedParseFloat; +export const limitedParseFloat = limitedParseDouble; + +/** + * Asserts a value is a 32-bit float and returns it. If the value is a string + * representation of a non-numeric number type (NaN, Infinity, -Infinity), + * the value will be parsed. Any other string value will result in an exception + * being thrown. Null or undefined will be returned as undefined. Any other + * type will result in an exception being thrown. + * + * @param value A number or string representation of a non-numeric float. + * @returns The value as a number, or undefined if it's null/undefined. + */ +export const limitedParseFloat32 = (value: string | number): number | undefined => { + if (typeof value == "string") { + switch (value) { + case "NaN": + return NaN; + case "Infinity": + return Infinity; + case "-Infinity": + return -Infinity; + default: + throw new Error(`Unable to parse float value: ${value}`); + } + } + return expectFloat32(value); +}; /** * Parses a value into an integer. If the value is null or undefined, undefined @@ -150,12 +310,73 @@ export const handleFloat = limitedParseFloat; * @param value A number or string representation of an integer. * @returns The value as a number, or undefined if it's null/undefined. */ -export const strictParseInt = (value: string | number): number | undefined => { +export const strictParseLong = (value: string | number): number | undefined => { + if (typeof value === "string") { + // parseInt can't be used here, because it will silently discard any + // existing decimals. We want to instead throw an error if there are any. + return expectLong(parseFloat(value)); + } + return expectLong(value); +}; + +/** + * @deprecated Use strictParseLong + */ +export const strictParseInt = strictParseLong; + +/** + * Parses a value into a 32-bit integer. If the value is null or undefined, undefined + * will be returned. If the value is a string, it will be parsed by parseFloat + * and the result will be asserted to be an integer. If the parsed value is not + * an integer, or the raw value is any type other than a string or number, an + * exception will be thrown. + * + * @param value A number or string representation of a 32-bit integer. + * @returns The value as a number, or undefined if it's null/undefined. + */ +export const strictParseInt32 = (value: string | number): number | undefined => { + if (typeof value === "string") { + // parseInt can't be used here, because it will silently discard any + // existing decimals. We want to instead throw an error if there are any. + return expectInt32(parseFloat(value)); + } + return expectInt32(value); +}; + +/** + * Parses a value into a 16-bit integer. If the value is null or undefined, undefined + * will be returned. If the value is a string, it will be parsed by parseFloat + * and the result will be asserted to be an integer. If the parsed value is not + * an integer, or the raw value is any type other than a string or number, an + * exception will be thrown. + * + * @param value A number or string representation of a 16-bit integer. + * @returns The value as a number, or undefined if it's null/undefined. + */ +export const strictParseShort = (value: string | number): number | undefined => { + if (typeof value === "string") { + // parseInt can't be used here, because it will silently discard any + // existing decimals. We want to instead throw an error if there are any. + return expectShort(parseFloat(value)); + } + return expectShort(value); +}; + +/** + * Parses a value into an 8-bit integer. If the value is null or undefined, undefined + * will be returned. If the value is a string, it will be parsed by parseFloat + * and the result will be asserted to be an integer. If the parsed value is not + * an integer, or the raw value is any type other than a string or number, an + * exception will be thrown. + * + * @param value A number or string representation of an 8-bit integer. + * @returns The value as a number, or undefined if it's null/undefined. + */ +export const strictParseByte = (value: string | number): number | undefined => { if (typeof value === "string") { // parseInt can't be used here, because it will silently discard any // existing decimals. We want to instead throw an error if there are any. - // TODO: handle underflow / overflow explicitly - return expectInt(parseFloat(value)); + return expectByte(parseFloat(value)); } - return expectInt(value); + return expectByte(value); };