feat(shared): add signed percent and percentage-point formatters (WealthLedger/main#54)
This commit is contained in:
@@ -3,12 +3,22 @@ import { describe, expect, it } from "vitest";
|
|||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
formatMoney,
|
formatMoney,
|
||||||
|
formatPercentagePoints,
|
||||||
|
formatSignedPercent,
|
||||||
formatSnapshotDay,
|
formatSnapshotDay,
|
||||||
formatSnapshotTime,
|
formatSnapshotTime,
|
||||||
instrumentLogoUrl,
|
instrumentLogoUrl,
|
||||||
} from "./display";
|
} from "./display";
|
||||||
|
|
||||||
describe("shared display helpers", () => {
|
describe("shared display helpers", () => {
|
||||||
|
it("formats signed rates as percentages and percentage points without negative zero", () => {
|
||||||
|
expect(formatSignedPercent("0.151")).toBe("+15,1 %");
|
||||||
|
expect(formatSignedPercent("-0.15149")).toBe("−15,1 %");
|
||||||
|
expect(formatSignedPercent("0")).toBe("0,0 %");
|
||||||
|
expect(formatSignedPercent("-0.00049")).toBe("0,0 %");
|
||||||
|
expect(formatPercentagePoints("0.043")).toBe("+4,3 п.п.");
|
||||||
|
expect(formatPercentagePoints("-0.0435")).toBe("−4,4 п.п.");
|
||||||
|
});
|
||||||
it("uses padded Russian dates and preserves the UTC date of timestamps", () => {
|
it("uses padded Russian dates and preserves the UTC date of timestamps", () => {
|
||||||
expect(formatDate("2027-09-10")).toBe("10.09.2027");
|
expect(formatDate("2027-09-10")).toBe("10.09.2027");
|
||||||
expect(formatDate("2024-02-29")).toBe("29.02.2024");
|
expect(formatDate("2024-02-29")).toBe("29.02.2024");
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { decimalFromString } from "./decimal";
|
||||||
|
|
||||||
const DECIMAL_PART_COUNT = 2;
|
const DECIMAL_PART_COUNT = 2;
|
||||||
const DEFAULT_MINOR_UNITS = 2;
|
const DEFAULT_MINOR_UNITS = 2;
|
||||||
|
|
||||||
@@ -99,6 +101,29 @@ function formatMoney(
|
|||||||
return value.startsWith("-") ? `−${formatted.slice(1)}` : `+${formatted}`;
|
return value.startsWith("-") ? `−${formatted.slice(1)}` : `+${formatted}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatPercentagePoints(rate: string): string {
|
||||||
|
return formatRate(rate, "п.п.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRate(rate: string, suffix: string): string {
|
||||||
|
const { scale, value } = decimalFromString(rate);
|
||||||
|
const abs = value < 0n ? -value : value;
|
||||||
|
const radix = 10n;
|
||||||
|
const half = 2n;
|
||||||
|
const tenthsPerUnit = 1000n;
|
||||||
|
const denominator = radix ** BigInt(scale);
|
||||||
|
const tenths = (abs * tenthsPerUnit + denominator / half) / denominator;
|
||||||
|
let sign = "";
|
||||||
|
if (tenths !== 0n) sign = value < 0n ? "−" : "+";
|
||||||
|
const formatted = `${formatDecimal(String(tenths / radix))},${tenths % radix}`;
|
||||||
|
|
||||||
|
return `${sign}${formatted} ${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSignedPercent(rate: string): string {
|
||||||
|
return formatRate(rate, "%");
|
||||||
|
}
|
||||||
|
|
||||||
function isZeroDecimal(value: string) {
|
function isZeroDecimal(value: string) {
|
||||||
const unsigned = value.startsWith("-") ? value.slice(1) : value;
|
const unsigned = value.startsWith("-") ? value.slice(1) : value;
|
||||||
|
|
||||||
@@ -171,6 +196,8 @@ export {
|
|||||||
formatDate,
|
formatDate,
|
||||||
formatDecimal,
|
formatDecimal,
|
||||||
formatMoney,
|
formatMoney,
|
||||||
|
formatPercentagePoints,
|
||||||
|
formatSignedPercent,
|
||||||
formatSnapshotDay,
|
formatSnapshotDay,
|
||||||
formatSnapshotTime,
|
formatSnapshotTime,
|
||||||
instrumentLogoUrl,
|
instrumentLogoUrl,
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export {
|
|||||||
formatDate,
|
formatDate,
|
||||||
formatDecimal,
|
formatDecimal,
|
||||||
formatMoney,
|
formatMoney,
|
||||||
|
formatPercentagePoints,
|
||||||
|
formatSignedPercent,
|
||||||
formatSnapshotDay,
|
formatSnapshotDay,
|
||||||
formatSnapshotTime,
|
formatSnapshotTime,
|
||||||
instrumentLogoUrl,
|
instrumentLogoUrl,
|
||||||
|
|||||||
Reference in New Issue
Block a user