Skip to content

Commit

Permalink
feat: big num new formatting rules (#6106)
Browse files Browse the repository at this point in the history
* feat: big num new formatting rules

* solidify ranges

* Format higher magnitudes as per PRD

* Treat 0 int as count of 0

* Update tests

* Update big num values in tests

* Leftover test values

* Update comparison test

* Update range
  • Loading branch information
djbarnwal authored Nov 21, 2024
1 parent 82e003a commit c24cd4e
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 93 deletions.
122 changes: 56 additions & 66 deletions web-common/src/lib/number-formatting/format-measure-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@ import { format as d3format } from "d3-format";
import {
FormatPreset,
NumberKind,
type ContextOptions,
type FormatterContext,
type FormatterContextSurface,
} from "./humanizer-types";
import {
formatMsInterval,
formatMsToDuckDbIntervalString,
} from "./strategies/intervals";
import { PerRangeFormatter } from "./strategies/per-range";
import {
bigNumCurrencyOptions,
bigNumDefaultFormattingOptions,
bigNumPercentOptions,
} from "./strategies/per-range-bignum-options";
import {
defaultCurrencyOptions,
defaultGenericNumOptions,
Expand All @@ -23,51 +30,81 @@ import {
} from "./strategies/per-range-tooltip-options";

/**
* This function is intended to provides a compact,
* potentially lossy, humanized string representation of a number.
* This function provides a compact, potentially lossy, humanized string representation of a number.
* @param value The number to format
* @param preset The format preset to use
* @param type The format context type (e.g., tooltip, big-number)
*/
function humanizeDataType(value: number, preset: FormatPreset): string {
function humanizeDataType(
value: number,
preset: FormatPreset,
type: FormatterContextSurface,
): string {
if (typeof value !== "number") {
console.warn(
`humanizeDataType only accepts numbers, got ${value} for FormatPreset "${preset}"`,
);

return JSON.stringify(value);
}

const optionsMap: Record<FormatterContextSurface, ContextOptions> = {
tooltip: {
none: tooltipNoFormattingOptions,
currencyUsd: tooltipCurrencyOptions(NumberKind.DOLLAR),
currencyEur: tooltipCurrencyOptions(NumberKind.EURO),
percent: tooltipPercentOptions,
humanize: tooltipNoFormattingOptions,
},
"big-number": {
none: bigNumDefaultFormattingOptions,
currencyUsd: bigNumCurrencyOptions(NumberKind.DOLLAR),
currencyEur: bigNumCurrencyOptions(NumberKind.EURO),
percent: bigNumPercentOptions,
humanize: bigNumDefaultFormattingOptions,
},
table: {
none: defaultNoFormattingOptions,
currencyUsd: defaultCurrencyOptions(NumberKind.DOLLAR),
currencyEur: defaultCurrencyOptions(NumberKind.EURO),
percent: defaultPercentOptions,
humanize: defaultGenericNumOptions,
},
};

const selectedOptions = optionsMap[type] || optionsMap.table;

switch (preset) {
case FormatPreset.NONE:
return new PerRangeFormatter(defaultNoFormattingOptions).stringFormat(
return new PerRangeFormatter(selectedOptions.none).stringFormat(value);

case FormatPreset.CURRENCY_USD:
return new PerRangeFormatter(selectedOptions.currencyUsd).stringFormat(
value,
);
case FormatPreset.CURRENCY_USD:
return new PerRangeFormatter(
defaultCurrencyOptions(NumberKind.DOLLAR),
).stringFormat(value);

case FormatPreset.CURRENCY_EUR:
return new PerRangeFormatter(
defaultCurrencyOptions(NumberKind.EURO),
).stringFormat(value);
return new PerRangeFormatter(selectedOptions.currencyEur).stringFormat(
value,
);

case FormatPreset.PERCENTAGE:
return new PerRangeFormatter(defaultPercentOptions).stringFormat(value);
return new PerRangeFormatter(selectedOptions.percent).stringFormat(value);

case FormatPreset.INTERVAL:
return formatMsInterval(value);
return type === "tooltip"
? formatMsToDuckDbIntervalString(value)
: formatMsInterval(value);

case FormatPreset.HUMANIZE:
return new PerRangeFormatter(defaultGenericNumOptions).stringFormat(
return new PerRangeFormatter(selectedOptions.humanize).stringFormat(
value,
);

default:
console.warn(
"Unknown format preset, using none formatter. All number kinds should be handled.",
);
return new PerRangeFormatter(defaultNoFormattingOptions).stringFormat(
value,
);
return new PerRangeFormatter(selectedOptions.none).stringFormat(value);
}
}

Expand Down Expand Up @@ -97,51 +134,6 @@ function humanizeDataTypeUnabridged(value: number, type: FormatPreset): string {
return value.toString();
}

function humanizeDataTypeForTooltip(
value: number,
preset: FormatPreset,
): string {
if (typeof value !== "number") {
console.warn(
`humanizeDataType only accepts numbers, got ${value} for FormatPreset "${preset}"`,
);

return JSON.stringify(value);
}

switch (preset) {
case FormatPreset.CURRENCY_USD:
return new PerRangeFormatter(
tooltipCurrencyOptions(NumberKind.DOLLAR),
).stringFormat(value);

case FormatPreset.CURRENCY_EUR:
return new PerRangeFormatter(
tooltipCurrencyOptions(NumberKind.EURO),
).stringFormat(value);

case FormatPreset.PERCENTAGE:
return new PerRangeFormatter(tooltipPercentOptions).stringFormat(value);

case FormatPreset.INTERVAL:
return formatMsToDuckDbIntervalString(value);

case FormatPreset.HUMANIZE:
case FormatPreset.NONE:
return new PerRangeFormatter(tooltipNoFormattingOptions).stringFormat(
value,
);

default:
console.warn(
"Unknown format preset, using none formatter. All number kinds should be handled.",
);
return new PerRangeFormatter(tooltipNoFormattingOptions).stringFormat(
value,
);
}
}

/**
* This higher-order function takes a measure spec and returns
* a function appropriate for formatting values from that measure.
Expand All @@ -167,10 +159,8 @@ export function createMeasureValueFormatter<T extends null | undefined = never>(
let humanizer: (value: number, type: FormatPreset) => string;
if (useUnabridged) {
humanizer = humanizeDataTypeUnabridged;
} else if (isTooltip) {
humanizer = humanizeDataTypeForTooltip;
} else {
humanizer = humanizeDataType;
humanizer = (value, preset) => humanizeDataType(value, preset, type);
}

// Return and empty string if measureSpec is not provided.
Expand Down
10 changes: 10 additions & 0 deletions web-common/src/lib/number-formatting/humanizer-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,3 +308,13 @@ export type FormatterContext =
| "unabridged"
| "big-number"
| "tooltip";

export type FormatterContextSurface = Exclude<FormatterContext, "unabridged">;

export type ContextOptions = {
none: FormatterRangeSpecsStrategy;
currencyUsd: FormatterRangeSpecsStrategy;
currencyEur: FormatterRangeSpecsStrategy;
percent: FormatterRangeSpecsStrategy;
humanize: FormatterRangeSpecsStrategy;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import {
type FormatterOptionsCommon,
type FormatterRangeSpecsStrategy,
NumberKind,
type RangeFormatSpec,
} from "../humanizer-types";

const bigNumberRangeSpec: RangeFormatSpec[] = [
{
minMag: -10,
supMag: -4,
maxDigitsRight: 2,
baseMagnitude: 0,
overrideValue: {
int: "0",
dot: ".",
frac: "0001",
prefix: "<",
suffix: "",
},
},
{
minMag: -4,
supMag: 0,
maxDigitsLeft: 0,
maxDigitsRight: 4,
baseMagnitude: 0,
padWithInsignificantZeros: false,
},
{
minMag: 0,
supMag: 3,
maxDigitsLeft: 3,
maxDigitsRight: 2,
baseMagnitude: 0,
padWithInsignificantZeros: false,
},
{
minMag: 3,
supMag: 5,
maxDigitsRight: 0,
useTrailingDot: false,
baseMagnitude: 0,
maxDigitsLeft: 5,
padWithInsignificantZeros: false,
},
{
minMag: 5,
supMag: 6,
maxDigitsRight: 0,
useTrailingDot: false,
baseMagnitude: 3,
maxDigitsLeft: 3,
padWithInsignificantZeros: false,
},
{
minMag: 6,
supMag: 7,
maxDigitsRight: 2,
baseMagnitude: 6,
maxDigitsLeft: 1,
padWithInsignificantZeros: false,
},
{
minMag: 7,
supMag: 8,
maxDigitsRight: 1,
baseMagnitude: 6,
maxDigitsLeft: 2,
padWithInsignificantZeros: false,
},
{
minMag: 8,
supMag: 9,
maxDigitsRight: 0,
baseMagnitude: 6,
useTrailingDot: false,
maxDigitsLeft: 3,
padWithInsignificantZeros: false,
},
{
minMag: 9,
supMag: 10,
maxDigitsRight: 2,
baseMagnitude: 9,
maxDigitsLeft: 1,
padWithInsignificantZeros: false,
},
{
minMag: 10,
supMag: 11,
maxDigitsRight: 1,
baseMagnitude: 9,
maxDigitsLeft: 2,
padWithInsignificantZeros: false,
},
{
minMag: 11,
supMag: 12,
maxDigitsRight: 0,
baseMagnitude: 9,
useTrailingDot: false,
maxDigitsLeft: 3,
padWithInsignificantZeros: false,
},
];

export const bigNumDefaultFormattingOptions: FormatterOptionsCommon &
FormatterRangeSpecsStrategy = {
numberKind: NumberKind.ANY,
rangeSpecs: bigNumberRangeSpec,
defaultMaxDigitsRight: 2,
upperCaseEForExponent: true,
};

export const bigNumPercentOptions: FormatterOptionsCommon &
FormatterRangeSpecsStrategy = {
rangeSpecs: bigNumberRangeSpec,
defaultMaxDigitsRight: 2,
upperCaseEForExponent: true,
numberKind: NumberKind.PERCENT,
};

export const bigNumCurrencyOptions = (
numberKind: NumberKind,
): FormatterOptionsCommon & FormatterRangeSpecsStrategy => ({
rangeSpecs: bigNumberRangeSpec,
defaultMaxDigitsRight: 2,
upperCaseEForExponent: true,
numberKind,
});
6 changes: 4 additions & 2 deletions web-common/src/lib/number-formatting/utils/count-digits.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export const countDigits = (numStr: string) =>
numStr.replace(/[^0-9]/g, "").length;
export const countDigits = (numStr: string) => {
if (numStr === "0") return 0;
return numStr.replace(/[^0-9]/g, "").length;
};

export const countNonZeroDigits = (numStr: string) =>
numStr.replace(/[^1-9]/g, "").length;
10 changes: 5 additions & 5 deletions web-local/tests/explores/dimension-and-measure-selection.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect } from "@playwright/test";
import { useDashboardFlowTestSetup } from "web-local/tests/explores/dashboard-flow-test-setup";
import { test } from "../utils/test";
import { clickMenuButton } from "../utils/commonHelpers";
import { test } from "../utils/test";

test.describe("dimension and measure selectors", () => {
// dashboard test setup
Expand Down Expand Up @@ -31,17 +31,17 @@ test.describe("dimension and measure selectors", () => {
await escape();
await expect(measuresButton).toHaveText("1 of 2 Measures");

await expect(page.getByText("Sum of Bid Price 300.6k")).toBeVisible();
await expect(page.getByText("Total records 100.0k")).not.toBeVisible();
await expect(page.getByText("Sum of Bid Price 301k")).toBeVisible();
await expect(page.getByText("Total records 100k")).not.toBeVisible();

await measuresButton.click();
await clickMenuItem("Total records");
await clickMenuItem("Sum of Bid Price");
await expect(measuresButton).toHaveText("1 of 2 Measures");
await escape();

await expect(page.getByText("Sum of Bid Price 300.6k")).not.toBeVisible();
await expect(page.getByText("Total records 100.0k")).toBeVisible();
await expect(page.getByText("Sum of Bid Price 301k")).not.toBeVisible();
await expect(page.getByText("Total records 100k")).toBeVisible();

await dimensionsButton.click();
await clickMenuItem("Publisher");
Expand Down
Loading

1 comment on commit c24cd4e

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.