/* Copyright 2026 Marimo. All rights reserved. */ "use no memo"; import { type Column, makeStateUpdater, type RowData, type Table, type TableFeature, type Updater, } from "@tanstack/react-table"; import type { DataType } from "@/core/kernel/messages"; import { logNever } from "@/utils/assertNever"; import { prettyEngineeringNumber, prettyNumber, prettyScientificNumber, } from "@/utils/numbers"; import { memoizeLastValue } from "@/utils/once"; import type { ColumnFormattingOptions, ColumnFormattingState, ColumnFormattingTableState, FormatOption, } from "./types"; export const ColumnFormattingFeature: TableFeature = { // define the column formatting's initial state getInitialState: (state): ColumnFormattingTableState => { return { columnFormatting: {}, ...state, }; }, // define the new column formatting's default options getDefaultOptions: ( table: Table, ): ColumnFormattingOptions => { return { enableColumnFormatting: true, onColumnFormattingChange: makeStateUpdater("columnFormatting", table), locale: navigator.language, } as ColumnFormattingOptions; }, createColumn: ( column: Column, table: Table, ) => { column.getColumnFormatting = () => { return table.getState().columnFormatting[column.id]; }; column.getCanFormat = () => { return ( (table.options.enableColumnFormatting && column.columnDef.meta?.dataType !== "unknown" && column.columnDef.meta?.dataType !== undefined) ?? false ); }; column.setColumnFormatting = (value) => { const safeUpdater: Updater = (old) => { return { ...old, [column.id]: value, }; }; table.options.onColumnFormattingChange?.(safeUpdater); }; // apply column formatting column.applyColumnFormatting = (value) => { const dataType = column.columnDef.meta?.dataType; const format = column.getColumnFormatting?.(); if (format) { return applyFormat(value, { format, dataType, locale: table.options.locale, }); } return value; }; }, }; export const getFormatters = memoizeLastValue((locale: string) => { const percentFormatter = new Intl.NumberFormat(locale, { style: "percent", minimumFractionDigits: 0, maximumFractionDigits: 2, }); const dateFormatter = new Intl.DateTimeFormat(locale, { dateStyle: "short", // 3/4/2024 }); const dateTimeFormatter = new Intl.DateTimeFormat(locale, { dateStyle: "short", // 3/4/2024 timeStyle: "long", // 3:04:05 PM timeZone: "UTC", }); const timeFormatter = new Intl.DateTimeFormat(locale, { timeStyle: "long", // 3:04:05 PM timeZone: "UTC", }); const integerFormatter = new Intl.NumberFormat(locale, { maximumFractionDigits: 0, // 1,000,000 }); return { percentFormatter, dateFormatter, dateTimeFormatter, timeFormatter, integerFormatter, }; }); // Apply formatting to a value given a format and data type export const applyFormat = ( value: unknown, options: { format: FormatOption; dataType: DataType | undefined; locale: string; }, ) => { const { format, dataType, locale } = options; const { percentFormatter, dateFormatter, dateTimeFormatter, timeFormatter, integerFormatter, } = getFormatters(locale); // If the value is null, return an empty string if (value === null || value === undefined || value === "") { return ""; } // Handle date, number, string and boolean formatting switch (dataType) { case "time": // Do nothing return value; case "datetime": case "date": { const date = new Date(value as string); switch (format) { case "Date": return dateFormatter.format(date); case "Datetime": return dateTimeFormatter.format(date); case "Time": return timeFormatter.format(date); default: return value; } } case "integer": case "number": { const num = Number.parseFloat(value as string); switch (format) { case "Auto": return prettyNumber(num, locale); case "Percent": return percentFormatter.format(num); case "Scientific": return prettyScientificNumber(num, { shouldRound: true, locale }); case "Engineering": return prettyEngineeringNumber(num, locale); case "Integer": return integerFormatter.format(num); default: return value; } } case "string": switch (format) { case "Uppercase": return (value as string).toUpperCase(); case "Lowercase": return (value as string).toLowerCase(); case "Capitalize": return ( (value as string).charAt(0).toUpperCase() + (value as string).slice(1) ); case "Title": return (value as string) .split(" ") .map( (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), ) .join(" "); default: return value; } case "boolean": switch (format) { case "Yes/No": return (value as boolean) ? "Yes" : "No"; case "On/Off": return (value as boolean) ? "On" : "Off"; default: return value; } case undefined: case "unknown": return value; default: logNever(dataType); return value; } }; export function formattingExample( format: FormatOption, locale: string, ): string | number | undefined | null { switch (format) { case "Date": return String( applyFormat(new Date(), { format: "Date", dataType: "date", locale }), ); case "Datetime": return String( applyFormat(new Date(), { format: "Datetime", dataType: "date", locale, }), ); case "Time": return String( applyFormat(new Date(), { format: "Time", dataType: "date", locale }), ); case "Percent": return String( applyFormat(0.1234, { format: "Percent", dataType: "number", locale }), ); case "Scientific": return String( applyFormat(12_345_678_910, { format: "Scientific", dataType: "number", locale, }), ); case "Engineering": return String( applyFormat(12_345_678_910, { format: "Engineering", dataType: "number", locale, }), ); case "Integer": return String( applyFormat(1234.567, { format: "Integer", dataType: "number", locale, }), ); case "Auto": return String( applyFormat(1234.567, { format: "Auto", dataType: "number", locale }), ); default: return null; } }