Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 additions & 2 deletions src/helpers/test/units.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expect, test } from "vitest";
import {
type Cents,
centsToDollarsFormatted,
dollarsToCents,
priceWholeToCents,
} from "../units.ts";

Expand All @@ -22,7 +23,7 @@ test("price whole to cents", () => {
["$1.000", 100],

["$1.23", 123],
["$1.234", 123.4],
["$1.234", 123],

// formatted as numbers
["0", 0],
Expand All @@ -31,7 +32,7 @@ test("price whole to cents", () => {
["100", 100_00],

["1.23", 123],
["1.234", 123.4],
["1.234", 123],

// nested quotes (double)
['"$0"', 0],
Expand Down Expand Up @@ -91,3 +92,61 @@ test("cents to dollars formatted", () => {
expect(result).toEqual(expected);
}
});

test("dollarsToCents returns integer cents without floating point errors", () => {
// These prices are known to produce floating point precision errors
// e.g. 17.60 * 100 = 1760.0000000000002 in IEEE 754
const cases: [number, number][] = [
[17.6, 1760],
[12.5, 1250],
[9.99, 999],
[0.1, 10],
[3.33, 333],
[7.77, 777],
[11.11, 1111],
[20.0, 2000],
[0.01, 1],
[99.99, 9999],
];

for (const [dollars, expectedCents] of cases) {
const cents = dollarsToCents(dollars);
expect(cents).toBe(expectedCents);
expect(Number.isInteger(cents)).toBe(true);
}
});

test("priceWholeToCents returns integer cents for prices with floating point issues", () => {
// Regression test: $17.60 was sent to API as 1760.0000000000002
// which failed deserialization as i64
const cases: [string, number][] = [
["$17.60", 1760],
["17.60", 1760],
["$12.50", 1250],
["$9.99", 999],
["$0.10", 10],
["$20.00", 2000],
];

for (const [input, expectedCents] of cases) {
const { cents, invalid } = priceWholeToCents(input);
expect(invalid).toBe(false);
expect(cents).toBe(expectedCents);
expect(Number.isInteger(cents)).toBe(true);
}
});

test("priceWholeToCents handles numeric input without floating point errors", () => {
const cases: [number, number][] = [
[17.6, 1760],
[12.5, 1250],
[9.99, 999],
];

for (const [input, expectedCents] of cases) {
const { cents, invalid } = priceWholeToCents(input);
expect(invalid).toBe(false);
expect(cents).toBe(expectedCents);
expect(Number.isInteger(cents)).toBe(true);
}
});
6 changes: 3 additions & 3 deletions src/helpers/units.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function priceWholeToCents(
return { cents: null, invalid: true };
}

return { cents: price * 100, invalid: false };
return { cents: Math.round(price * 100), invalid: false };
} else if (typeof price === "string") {
// remove any whitespace, dollar signs, negative signs, single and double quotes
const priceCleaned = price.replace(/[\s$\-'"]/g, "");
Expand All @@ -97,7 +97,7 @@ export function priceWholeToCents(

const parsedPrice = Number.parseFloat(priceCleaned);

return { cents: parsedPrice * 100, invalid: false };
return { cents: Math.round(parsedPrice * 100), invalid: false };
}

// default invalid
Expand All @@ -113,7 +113,7 @@ export function centsToDollars(cents: Cents): number {
}

export function dollarsToCents(dollars: number): Cents {
return Math.ceil(dollars * 100);
return Math.round(dollars * 100);
}
export function parseStartDateOrNow(startDate?: string): Date | "NOW" {
if (!startDate) return "NOW";
Expand Down
Loading