"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseTimestamp = exports.parseDate = exports.formatDateWithPattern = exports.formatTemporalCellForSpreadsheet = exports.getFormatterTimezone = exports.isTemporalValue = exports.isCalendarValueItem = exports.isItemTimezoneAffected = exports.shouldShiftItemTimezone = exports.toIsoWithProjectOffset = exports.isTimestampString = exports.isMomentInput = exports.getDateFormat = exports.formatBoolean = exports.currencies = void 0;
exports.formatDate = formatDate;
exports.formatTimestamp = formatTimestamp;
exports.toExcelWallClockDate = toExcelWallClockDate;
exports.getLocalTimeDisplay = getLocalTimeDisplay;
exports.valueIsNaN = valueIsNaN;
exports.isNumber = isNumber;
exports.formatNumberValue = formatNumberValue;
exports.applyDefaultFormat = applyDefaultFormat;
exports.getCustomFormatFromLegacy = getCustomFormatFromLegacy;
exports.hasFormatting = hasFormatting;
exports.getEffectiveSeparator = getEffectiveSeparator;
exports.getCustomFormat = getCustomFormat;
exports.formatValueWithExpression = formatValueWithExpression;
exports.applyCustomFormat = applyCustomFormat;
exports.hasValidFormatExpression = hasValidFormatExpression;
exports.convertCustomFormatToFormatExpression = convertCustomFormatToFormatExpression;
exports.getFormatExpression = getFormatExpression;
exports.getExcelFormatExpression = getExcelFormatExpression;
exports.formatItemValue = formatItemValue;
const tslib_1 = require("tslib");
const dayjs_1 = tslib_1.__importDefault(require("dayjs"));
const timezone_1 = tslib_1.__importDefault(require("dayjs/plugin/timezone"));
const moment_timezone_1 = tslib_1.__importDefault(require("moment-timezone"));
const numfmt_1 = require("numfmt");
const parameters_1 = require("../compiler/parameters");
const field_1 = require("../types/field");
const metricQuery_1 = require("../types/metricQuery");
const timeFrames_1 = require("../types/timeFrames");
const assertUnreachable_1 = tslib_1.__importDefault(require("./assertUnreachable"));
const conditionalFormatExpressions_1 = require("./conditionalFormatExpressions");
const item_1 = require("./item");
dayjs_1.default.extend(timezone_1.default);
exports.currencies = [
    'USD',
    'EUR',
    'GBP',
    'JPY',
    'DKK',
    'CHF',
    'CAD',
    'AUD',
    'CNY',
    'ARS',
    'BRL',
    'CLP',
    'COP',
    'CZK',
    'HKD',
    'HUF',
    'INR',
    'ILS',
    'KRW',
    'MYR',
    'MXN',
    'MAD',
    'NZD',
    'NOK',
    'PHP',
    'PLN',
    'RUB',
    'SAR',
    'SGD',
    'ZAR',
    'SEK',
    'TWD',
    'THB',
    'TRY',
    'VND',
];
const formatBoolean = (v) => ['True', 'true', 'yes', 'Yes', '1', 'T'].includes(`${v}`)
    ? 'True'
    : 'False';
exports.formatBoolean = formatBoolean;
const getDateFormat = (timeInterval = timeFrames_1.TimeFrames.DAY) => {
    switch (timeInterval) {
        case timeFrames_1.TimeFrames.YEAR:
            return 'YYYY';
        case timeFrames_1.TimeFrames.QUARTER:
            return 'YYYY-[Q]Q';
        case timeFrames_1.TimeFrames.MONTH:
            return 'YYYY-MM';
        default:
            return 'YYYY-MM-DD';
    }
};
exports.getDateFormat = getDateFormat;
const getTimeFormat = (timeInterval = timeFrames_1.TimeFrames.DAY) => {
    let timeFormat;
    switch (timeInterval) {
        case timeFrames_1.TimeFrames.HOUR:
            timeFormat = 'HH';
            break;
        case timeFrames_1.TimeFrames.MINUTE:
            timeFormat = 'HH:mm';
            break;
        case timeFrames_1.TimeFrames.SECOND:
            timeFormat = 'HH:mm:ss';
            break;
        default:
            timeFormat = 'HH:mm:ss:SSS';
            break;
    }
    return `YYYY-MM-DD, ${timeFormat} (Z)`;
};
// TODO: To rename to isDayJsInput once we remove moment usage
const isMomentInput = (value) => typeof value === 'string' ||
    typeof value === 'number' ||
    value instanceof Date ||
    value instanceof moment_timezone_1.default ||
    value instanceof dayjs_1.default;
exports.isMomentInput = isMomentInput;
// A string carrying both a date and a time component (e.g. "2024-01-02T03:04…"
// or "2024-01-02 03:04…") — as opposed to a bare date, a number, or text.
// Unlike a parse-validity check, this requires a time part, so date-only values
// are excluded (they must not be timezone-shifted).
const isTimestampString = (value) => typeof value === 'string' &&
    /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/.test(value);
exports.isTimestampString = isTimestampString;
function formatDate(date, timeInterval = timeFrames_1.TimeFrames.DAY, convertToUTC = false, timezone) {
    let momentDate;
    if (timezone) {
        momentDate = moment_timezone_1.default.utc(date).tz(timezone);
    }
    else if (convertToUTC) {
        momentDate = moment_timezone_1.default.utc(date);
    }
    else {
        momentDate = (0, moment_timezone_1.default)(date);
    }
    if (!momentDate.isValid()) {
        return 'NaT';
    }
    return momentDate.format((0, exports.getDateFormat)(timeInterval));
}
// Pass `timezone` to convert the UTC value into that zone. Pass `displayTimezone`
// when the value is already wall-clock in that zone — it only appends the offset
// suffix. Pass at most one.
function formatTimestamp(value, timeInterval = timeFrames_1.TimeFrames.MILLISECOND, convertToUTC = false, timezone, displayTimezone) {
    let momentDate;
    if (timezone) {
        momentDate = moment_timezone_1.default.utc(value).tz(timezone);
    }
    else if (convertToUTC) {
        momentDate = moment_timezone_1.default.utc(value);
    }
    else {
        momentDate = (0, moment_timezone_1.default)(value);
    }
    if (!momentDate.isValid()) {
        return 'NaT';
    }
    if (!timezone && displayTimezone) {
        const offsetMinutes = moment_timezone_1.default.tz(value, displayTimezone).utcOffset();
        return momentDate
            .utcOffset(offsetMinutes, true)
            .format(getTimeFormat(timeInterval));
    }
    return momentDate.format(getTimeFormat(timeInterval));
}
// Date whose UTC components match `value` rendered in `timezone`.
// ExcelJS serializes Date cells via UTC components and Excel cells
// carry no zone — this is how we land project-tz wall-clock in a
// real date cell. Resulting Date no longer represents a real instant.
function toExcelWallClockDate(value, timezone) {
    return moment_timezone_1.default.tz(value, timezone).utc(true).toDate();
}
// Re-encode a UTC instant as ISO 8601 in the project tz with an explicit
// offset suffix (e.g. `2024-01-14T15:00:00.000-11:00`). Returns undefined
// when there's nothing to shift; callers fall through to passthrough so
// flag-off / UTC output stays bit-identical.
const toIsoWithProjectOffset = (rawValue, timezone) => {
    if (!timezone || timezone === 'UTC')
        return undefined;
    if (typeof rawValue !== 'string' &&
        typeof rawValue !== 'number' &&
        !(rawValue instanceof Date)) {
        return undefined;
    }
    const m = moment_timezone_1.default.utc(rawValue);
    if (!m.isValid())
        return undefined;
    return m.tz(timezone).toISOString(true);
};
exports.toIsoWithProjectOffset = toIsoWithProjectOffset;
// Use this when deciding whether to re-shift a fetched VALUE in the client
// (chart rendering, spreadsheet exports). Only TIMESTAMP instants shift; calendar
// DATEs (incl. day-or-coarser truncs, which now compile to a real DATE —
// GLITCH-452) and `skipTimezoneConversion` dims stay put. Keyed off getItemType
// (resolved type) so it also classifies non-field items (table calcs, custom dims).
// NOT for "is this field timezone-sensitive" UI — use isItemTimezoneAffected.
const shouldShiftItemTimezone = (item) => {
    if (!item)
        return false;
    if ((0, field_1.isDimension)(item) && item.skipTimezoneConversion)
        return false;
    return (0, item_1.getItemType)(item) === field_1.DimensionType.TIMESTAMP;
};
exports.shouldShiftItemTimezone = shouldShiftItemTimezone;
// Use this for "is this field timezone-sensitive" UI affordances (icons,
// tooltips). Whether a field's value depends on the chart's timezone at all —
// server-side or client-side. Differs from shouldShiftItemTimezone ("does the
// client re-shift the value"): a day-or-coarser trunc of a TIMESTAMP base
// compiles to a real DATE, so it isn't re-shifted client-side, yet the backend
// shifts to project tz before truncating — so it IS timezone-affected. Keyed
// off the base column type, mirroring MetricQueryBuilder.
const isItemTimezoneAffected = (item) => {
    if (!item)
        return false;
    if ((0, field_1.isDimension)(item) && item.skipTimezoneConversion)
        return false;
    const baseType = (0, field_1.isDimension)(item) && item.timeIntervalBaseDimensionType
        ? item.timeIntervalBaseDimensionType
        : (0, item_1.getItemType)(item);
    return baseType === field_1.DimensionType.TIMESTAMP;
};
exports.isItemTimezoneAffected = isItemTimezoneAffected;
// A calendar value is a bare wall-clock date (year/month/day, no instant) that
// must never be timezone-shifted. GLITCH-452: every DATE-typed item is now a
// calendar value — plain DATE columns, DATE-base truncs, DATE metrics, and
// day-or-coarser truncs of a TIMESTAMP base (which now compile to a real DATE).
// Only TIMESTAMP-typed items are instants. Keyed off getItemType so it also
// covers DATE metrics and table calcs.
const isCalendarValueItem = (item) => {
    if (!item)
        return false;
    return (0, item_1.getItemType)(item) === field_1.DimensionType.DATE;
};
exports.isCalendarValueItem = isCalendarValueItem;
// A Date or a datetime string (not a date-only string).
const isTemporalValue = (value) => value instanceof Date || (0, exports.isTimestampString)(value);
exports.isTemporalValue = isTemporalValue;
// Which timezone a formatter applies to a value. The shape predicate decides
// every type-known item (dims, metrics, table calcs, custom dims). MIN/MAX are
// the one opaque case — their aggregation type hides the temporal base — so only
// there do we fall back to the value shape.
const getFormatterTimezone = (item, value, timezone) => {
    if (!timezone)
        return undefined;
    if ((0, field_1.isDimension)(item) && item.skipTimezoneConversion)
        return undefined;
    if ((0, exports.isCalendarValueItem)(item))
        return undefined;
    if ((0, exports.shouldShiftItemTimezone)(item))
        return timezone;
    const type = item ? (0, item_1.getItemType)(item) : undefined;
    if ((type === field_1.MetricType.MIN || type === field_1.MetricType.MAX) &&
        (0, exports.isTemporalValue)(value)) {
        return timezone;
    }
    return undefined;
};
exports.getFormatterTimezone = getFormatterTimezone;
// Renders a temporal cell as the wall-clock string Excel and Google Sheets
// auto-detect as a date. TIMESTAMP → `YYYY-MM-DD HH:mm:ss.SSS`, DATE →
// `YYYY-MM-DD`. Shifts into the project tz when the item is timezone-
// shiftable (TIMESTAMP only — GLITCH-452 makes day-grain DATEs calendar values)
// and a timezone is supplied; calendar DATEs are formatted as-is. Returns undefined for non-temporal fields or
// unparseable values so callers can fall through to their existing
// formatting path.
const formatTemporalCellForSpreadsheet = (item, rawValue, timezone) => {
    if (!(0, field_1.isField)(item))
        return undefined;
    const isTimestamp = item.type === field_1.DimensionType.TIMESTAMP;
    const isDate = item.type === field_1.DimensionType.DATE;
    if (!isTimestamp && !isDate)
        return undefined;
    if (rawValue === null || rawValue === undefined || rawValue === '') {
        return undefined;
    }
    const shouldShift = !!timezone && (0, exports.shouldShiftItemTimezone)(item);
    const m = shouldShift
        ? moment_timezone_1.default.utc(rawValue).tz(timezone)
        : (0, moment_timezone_1.default)(rawValue);
    if (!m.isValid())
        return undefined;
    return m.format(isTimestamp ? 'YYYY-MM-DD HH:mm:ss.SSS' : 'YYYY-MM-DD');
};
exports.formatTemporalCellForSpreadsheet = formatTemporalCellForSpreadsheet;
function getLocalTimeDisplay(value, showTimezone = true) {
    // NOTE: Mixing dayjs and moment here is not great, but we're doing it here
    // because we are using moment types in this file and the
    // plumbing expects them. It should be ok here because we are not moment and dayjs
    // together to operate on the date. Dayjs is only used for the
    // Timezone string, which moment doesn't support.
    const tzString = showTimezone ? `(${dayjs_1.default.tz.guess()})` : '';
    return `${(0, moment_timezone_1.default)(value).format(`YYYY-MM-DD HH:mm`)} ${tzString}`;
}
const formatDateWithPattern = (value, pattern) => {
    const m = (0, moment_timezone_1.default)(value).utc();
    if (!m.isValid())
        return String(value);
    return m.format(pattern);
};
exports.formatDateWithPattern = formatDateWithPattern;
const parseDate = (str, timeInterval = timeFrames_1.TimeFrames.DAY) => (0, moment_timezone_1.default)(str, (0, exports.getDateFormat)(timeInterval)).toDate();
exports.parseDate = parseDate;
const parseTimestamp = (str, timeInterval = timeFrames_1.TimeFrames.MILLISECOND) => (0, moment_timezone_1.default)(str, getTimeFormat(timeInterval)).toDate();
exports.parseTimestamp = parseTimestamp;
function getFormatNumberOptions(value, format) {
    const hasCurrency = format?.type === field_1.CustomFormatType.CURRENCY && format?.currency;
    const currencyOptions = hasCurrency
        ? { style: 'currency', currency: format.currency }
        : {};
    const round = format?.round;
    if (round === undefined) {
        // When round is not defined, keep up to 3 decimal places
        return hasCurrency ? currencyOptions : {};
    }
    if (round < 0) {
        return {
            maximumSignificantDigits: Math.max(Math.floor(value).toString().length + round, 1),
            maximumFractionDigits: 0,
            ...currencyOptions,
        };
    }
    const fractionDigits = Math.min(round, 20);
    return {
        maximumFractionDigits: fractionDigits,
        minimumFractionDigits: fractionDigits,
        ...currencyOptions,
    };
}
function valueIsNaN(value) {
    if (typeof value === 'boolean')
        return true;
    return Number.isNaN(Number(value));
}
function isNumber(value) {
    return !valueIsNaN(value);
}
// Separator styles whose grouping/decimal characters have no exact built-in
// numfmt locale are registered here once, at module load. The characters mirror
// the `toLocaleString`-based CustomFormat path so an ECMA-376 expression and a
// structured format render the same separators. The registry is immutable after
// this — the separator is selected per-call via numfmt's `locale` option, which
// is safe for concurrent multi-tenant formatting on the server.
// Tags must be valid BCP-47 (numfmt rejects 3+ hyphenated parts as malformed
// and silently falls back to its default locale), so use short 2-segment tags.
const NUMFMT_LOCALE_PERIOD_COMMA = 'ld-pc'; // 1.234.567,50
const NUMFMT_LOCALE_SPACE_PERIOD = 'ld-sp'; // 1 234 567.50
const NUMFMT_LOCALE_NO_SEPARATOR = 'ld-ns'; // 1234567.50
const NUMFMT_LOCALE_APOSTROPHE_PERIOD = 'ld-ap'; // 1'234'567.50
(0, numfmt_1.addLocale)({ group: '.', decimal: ',' }, NUMFMT_LOCALE_PERIOD_COMMA);
(0, numfmt_1.addLocale)({ group: ' ', decimal: '.' }, NUMFMT_LOCALE_SPACE_PERIOD);
(0, numfmt_1.addLocale)({ group: '', decimal: '.' }, NUMFMT_LOCALE_NO_SEPARATOR);
(0, numfmt_1.addLocale)({ group: "'", decimal: '.' }, NUMFMT_LOCALE_APOSTROPHE_PERIOD);
// The numfmt locale used to render an ECMA-376 expression for a given separator.
// DEFAULT and COMMA_PERIOD return undefined so numfmt keeps its built-in
// comma-period output, leaving existing format strings byte-identical.
function separatorToNumfmtLocale(separator) {
    switch (separator) {
        case field_1.NumberSeparator.PERIOD_COMMA:
            return NUMFMT_LOCALE_PERIOD_COMMA;
        case field_1.NumberSeparator.SPACE_PERIOD:
            return NUMFMT_LOCALE_SPACE_PERIOD;
        case field_1.NumberSeparator.NO_SEPARATOR_PERIOD:
            return NUMFMT_LOCALE_NO_SEPARATOR;
        case field_1.NumberSeparator.APOSTROPHE_PERIOD:
            return NUMFMT_LOCALE_APOSTROPHE_PERIOD;
        case field_1.NumberSeparator.COMMA_PERIOD:
        case field_1.NumberSeparator.DEFAULT:
        case undefined:
            return undefined;
        default:
            return (0, assertUnreachable_1.default)(separator, `Unknown number separator ${separator}`);
    }
}
function formatNumberValue(value, format) {
    const options = getFormatNumberOptions(value, format);
    const separator = format?.separator || field_1.NumberSeparator.DEFAULT;
    switch (separator) {
        case field_1.NumberSeparator.COMMA_PERIOD:
            return value.toLocaleString('en-US', options);
        case field_1.NumberSeparator.SPACE_PERIOD:
            return value.toLocaleString('en-US', options).replace(/,/g, ' ');
        case field_1.NumberSeparator.PERIOD_COMMA:
            // If currency is provided, having a PERIOD_COMMA separator will also change the position of the currency symbol
            return value.toLocaleString('de-DE', options);
        case field_1.NumberSeparator.NO_SEPARATOR_PERIOD:
            return value.toLocaleString('en-US', {
                ...options,
                useGrouping: false,
            });
        case field_1.NumberSeparator.APOSTROPHE_PERIOD:
            return value.toLocaleString('en-US', options).replace(/,/g, "'");
        case field_1.NumberSeparator.DEFAULT:
            // This will apply the default style for each currency
            return value.toLocaleString(undefined, options);
        default:
            return (0, assertUnreachable_1.default)(separator, 'Unknown separator');
    }
}
function applyDefaultFormat(value) {
    if (value === null)
        return '∅';
    if (value === undefined)
        return '-';
    if (!isNumber(value)) {
        return `${value}`;
    }
    return formatNumberValue(value);
}
function getCustomFormatFromLegacy({ format, compact, round, }) {
    switch (format) {
        case field_1.Format.EUR:
        case field_1.Format.GBP:
        case field_1.Format.USD:
        case field_1.Format.JPY:
        case field_1.Format.DKK:
            return {
                type: field_1.CustomFormatType.CURRENCY,
                currency: format.toUpperCase(),
                compact,
                round,
            };
        case field_1.Format.KM:
        case field_1.Format.MI:
            return {
                type: field_1.CustomFormatType.NUMBER,
                suffix: ` ${format}`,
                compact,
                round,
            };
        case field_1.Format.PERCENT:
            return {
                type: field_1.CustomFormatType.PERCENT,
                compact,
                round,
            };
        case field_1.Format.ID:
            return {
                type: field_1.CustomFormatType.ID,
            };
        default:
            return {
                type: field_1.CustomFormatType.NUMBER,
                round,
                compact,
            };
    }
}
function hasFormatting(item) {
    if (!item)
        return false;
    if ((0, metricQuery_1.hasFormatOptions)(item)) {
        return true;
    }
    if ((0, field_1.isTableCalculation)(item)) {
        return item.format !== undefined;
    }
    if ((0, field_1.isDimension)(item) || (0, field_1.isMetric)(item)) {
        return (item.format !== undefined ||
            item.compact !== undefined ||
            item.round !== undefined);
    }
    return false;
}
// The effective number separator for an item, in priority order: an explicit
// formatOptions separator, a table calculation's format separator, then the
// field-level `separator` (the dbt YAML property). Used by both the structured
// and ECMA-376 rendering paths so they agree.
function getEffectiveSeparator(item) {
    if (!item)
        return undefined;
    if ((0, metricQuery_1.hasFormatOptions)(item) && item.formatOptions.separator) {
        return item.formatOptions.separator;
    }
    if ((0, field_1.isTableCalculation)(item) && item.format?.separator) {
        return item.format.separator;
    }
    if ('separator' in item && item.separator) {
        return item.separator;
    }
    return undefined;
}
function getCustomFormat(item) {
    if (!item)
        return undefined;
    let base;
    if ((0, metricQuery_1.hasFormatOptions)(item)) {
        base = item.formatOptions;
    }
    else if ((0, field_1.isTableCalculation)(item)) {
        base = item.format;
    }
    else {
        const legacyFormat = {
            ...('format' in item && { format: item.format }),
            ...('compact' in item && { compact: item.compact }),
            ...('round' in item && { round: item.round }),
        };
        // Only get custom format from legacy if there are any legacy format options or if the item is numeric
        if (Object.keys(legacyFormat).length > 0 || (0, item_1.isNumericItem)(item)) {
            base = getCustomFormatFromLegacy(legacyFormat);
        }
    }
    if (!base)
        return undefined;
    // Apply the field-level separator unless the format already carries one.
    // DEFAULT is a no-op, so skip the allocation for it.
    if (!base.separator) {
        const separator = getEffectiveSeparator(item);
        if (separator && separator !== field_1.NumberSeparator.DEFAULT) {
            return { ...base, separator };
        }
    }
    return base;
}
function applyCompact(value, format) {
    if (format?.compact === undefined)
        return { compactValue: Number(value), compactSuffix: '' };
    const compactConfig = (0, field_1.findCompactConfig)(format.compact);
    if (compactConfig) {
        const compactValue = compactConfig.convertFn(Number(value));
        const compactSuffix = format.compact ? compactConfig.suffix : '';
        return { compactValue, compactSuffix };
    }
    return { compactValue: Number(value), compactSuffix: '' };
}
function formatValueWithExpression(expression, value, locale, timezone) {
    try {
        let sanitizedValue = value;
        // Only number formatting is localised; dates/text keep the default
        // locale so month names etc. are unaffected.
        const localeOptions = locale ? { locale } : undefined;
        if (typeof value === 'bigint') {
            if (value <= Number.MAX_SAFE_INTEGER &&
                value >= Number.MIN_SAFE_INTEGER) {
                sanitizedValue = Number(value);
            }
            else {
                throw new Error("Can't format value as BigInt is out of safe integer range");
            }
        }
        // Check if this is a binary (IEC) byte unit format expression using KiB, MiB, etc.
        const binaryByteUnits = field_1.IECByteCompacts.map((compact) => field_1.CompactConfigMap[compact].suffix);
        const binarySuffixMatch = binaryByteUnits.find((unit) => expression.includes(`"${unit}"`));
        if (binarySuffixMatch && !valueIsNaN(Number(sanitizedValue))) {
            const compactConfig = Object.values(field_1.CompactConfigMap).find((config) => config.suffix === binarySuffixMatch &&
                field_1.IECByteCompacts.includes(config.compact));
            if (compactConfig) {
                const convertedValue = compactConfig.convertFn(Number(sanitizedValue));
                const baseExpression = expression.replace(`"${binarySuffixMatch}"`, '');
                const formattedNumber = (0, numfmt_1.format)(baseExpression, convertedValue, localeOptions);
                return `${formattedNumber}${binarySuffixMatch}`;
            }
        }
        // format date
        if ((0, numfmt_1.isDateFormat)(expression)) {
            if (!(0, exports.isMomentInput)(sanitizedValue)) {
                return 'NaT';
            }
            // Shift into the project tz then relabel wall-clock as UTC so
            // numfmt's `ignoreTimezone` renders it verbatim. Gated like
            // formatTimestamp (`if (timezone)`).
            const dateForExpression = timezone
                ? moment_timezone_1.default.utc(sanitizedValue).tz(timezone).utc(true).toDate()
                : (0, moment_timezone_1.default)(sanitizedValue).toDate();
            return (0, numfmt_1.format)(expression, dateForExpression, {
                ignoreTimezone: true,
            });
        }
        // format text
        if ((0, numfmt_1.isTextFormat)(expression)) {
            return (0, numfmt_1.format)(expression, sanitizedValue);
        }
        // format number
        return valueIsNaN(Number(sanitizedValue))
            ? `${value}` // Return the raw value as a string if it's not a number
            : (0, numfmt_1.format)(expression, Number(sanitizedValue), localeOptions);
    }
    catch (e) {
        // eslint-disable-next-line no-console
        console.error('Error formatting value with expression', e);
        return `${value}`;
    }
}
function applyCustomFormat(value, format, timezone) {
    if (format?.type === undefined)
        return applyDefaultFormat(value);
    if (value === '')
        return '';
    // No `item` here, so gate the timezone by value; item-aware callers resolve
    // it via getFormatterTimezone first.
    const effectiveTimezone = (0, exports.isTemporalValue)(value) ? timezone : undefined;
    if (value instanceof Date &&
        ![field_1.CustomFormatType.DATE, field_1.CustomFormatType.TIMESTAMP].includes(format.type)) {
        return formatTimestamp(value, undefined, false, effectiveTimezone);
    }
    // Timestamp strings are `valueIsNaN`, so the guard below would return them
    // raw. When a timezone is supplied, format them first so they render shifted.
    if (effectiveTimezone && (0, exports.isTimestampString)(value)) {
        switch (format.type) {
            case field_1.CustomFormatType.DATE:
                return formatDate(value, format?.timeInterval, false, effectiveTimezone);
            case field_1.CustomFormatType.TIMESTAMP:
                return formatTimestamp(value, format?.timeInterval, false, effectiveTimezone);
            case field_1.CustomFormatType.CUSTOM:
                return formatValueWithExpression(format.custom || '', value, separatorToNumfmtLocale(format.separator), effectiveTimezone);
            default:
                break;
        }
    }
    if (valueIsNaN(value) || value === null) {
        return applyDefaultFormat(value);
    }
    switch (format.type) {
        case field_1.CustomFormatType.ID:
            return `${value}`;
        case field_1.CustomFormatType.DEFAULT:
            return applyDefaultFormat(value);
        case field_1.CustomFormatType.PERCENT:
            const formatted = formatNumberValue(Number(value) * 100, format);
            return `${formatted}%`;
        case field_1.CustomFormatType.CURRENCY:
            const { compactValue, compactSuffix } = applyCompact(value, format);
            const currencyFormatted = formatNumberValue(compactValue, format).replace(/\u00A0/, ' ');
            return `${currencyFormatted}${compactSuffix}`;
        case field_1.CustomFormatType.DATE:
            return formatDate(value, format?.timeInterval, false, effectiveTimezone);
        case field_1.CustomFormatType.TIMESTAMP:
            return formatTimestamp(value, format?.timeInterval, false, effectiveTimezone);
        case field_1.CustomFormatType.NUMBER:
            const prefix = format.prefix || '';
            const suffix = format.suffix || '';
            const { compactValue: compactNumber, compactSuffix: compactNumberSuffix, } = applyCompact(value, format);
            const numberFormatted = formatNumberValue(compactNumber, format);
            return `${prefix}${numberFormatted}${compactNumberSuffix}${suffix}`;
        case field_1.CustomFormatType.BYTES_SI:
        case field_1.CustomFormatType.BYTES_IEC: {
            const { compactValue: bytesCompactValue, compactSuffix: bytesCompactSuffix, } = applyCompact(value, format);
            return `${formatNumberValue(bytesCompactValue, format)}${bytesCompactSuffix}`;
        }
        case field_1.CustomFormatType.CUSTOM:
            return formatValueWithExpression(format.custom || '', value, separatorToNumfmtLocale(format.separator), effectiveTimezone);
        default:
            return (0, assertUnreachable_1.default)(format.type, `Table calculation format type ${format.type} is not valid`);
    }
}
/**
 * Validates a format string that may contain parameter placeholders.
 * Strips ${...} placeholders before validation since numfmt doesn't understand them.
 */
function isValidFormatWithParameters(formatString) {
    // Check if format contains parameter placeholders
    const hasPlaceholders = formatString.includes('${');
    if (!hasPlaceholders) {
        return (0, numfmt_1.isValidFormat)(formatString);
    }
    // Strip out ${...} placeholders and validate what remains
    // This handles formats like: '${ld.parameters.currency=="usd"?"$":""}0,0.00'
    // After stripping: '0,0.00' which is valid
    const withoutPlaceholders = formatString.replace(/\$\{[^}]+\}/g, '');
    return (0, numfmt_1.isValidFormat)(withoutPlaceholders);
}
function hasValidFormatExpression(item) {
    // filter out legacy format that might be valid expressions. eg: usd
    if (!item || !('format' in item) || !item.format) {
        return false;
    }
    return (((0, field_1.isField)(item) || (0, metricQuery_1.isAdditionalMetric)(item)) &&
        typeof item.format === 'string' &&
        !(0, field_1.isFormat)(item.format) &&
        isValidFormatWithParameters(item.format));
}
const customFormatConversionFnMap = {
    separator: (formatExpression, format) => {
        if (!format.separator ||
            format.separator !== field_1.NumberSeparator.NO_SEPARATOR_PERIOD) {
            // Add thousands separator by default
            // Note: we don't support specific separators characters. This depends on the locale which we don't support as we format values in the server atm.
            return `#,##0`;
        }
        return formatExpression;
    },
    currency: (formatExpression, format) => {
        if (format.currency) {
            const mockAmount = 1;
            const mockCurrencyValue = new Intl.NumberFormat('en-US', {
                style: 'currency',
                currency: format.currency,
            }).format(mockAmount);
            // get the currency symbol/prefix + replace NBSP char
            const currencySymbolPrefix = mockCurrencyValue
                .substring(0, mockCurrencyValue.indexOf(mockAmount.toString()))
                .replace(/\u00A0/, ' ');
            return `[$${currencySymbolPrefix}]${formatExpression}`;
        }
        return formatExpression;
    },
    prefix: (formatExpression, format) => {
        if (format.prefix) {
            return `"${format.prefix}"${formatExpression}`;
        }
        return formatExpression;
    },
    round: (formatExpression, format) => {
        let round = 2;
        if (format.round !== undefined) {
            round = format.round;
        }
        else if (format.type === field_1.CustomFormatType.CURRENCY &&
            format.currency) {
            const mockCurrencyValue = new Intl.NumberFormat('en-US', {
                style: 'currency',
                currency: format.currency,
            }).format(1);
            // find how many round decimals the currency has
            round = mockCurrencyValue.includes('.')
                ? mockCurrencyValue.split('.')[1].length
                : 0;
        }
        else if (format.type === field_1.CustomFormatType.NUMBER) {
            round = null;
        }
        if (round === null) {
            // Formatting with null round means we want to show up to 3 decimal places
            return `${formatExpression}.###`;
        }
        if (round > 0) {
            return `${formatExpression}.${'0'.repeat(round)}`;
        }
        return formatExpression;
    },
    compact: (formatExpression, format) => {
        if (format.compact) {
            const compactConfig = (0, field_1.findCompactConfig)(format.compact);
            if (compactConfig) {
                // Check if this is a binary (IEC) byte unit, like KiB, MiB, etc.
                const isBinaryByteUnit = field_1.IECByteCompacts.includes(compactConfig.compact);
                if (isBinaryByteUnit) {
                    return `${formatExpression}"${compactConfig.suffix}"`;
                }
                return `${formatExpression}${','.repeat(compactConfig.orderOfMagnitude / 3)}"${compactConfig.suffix}"`;
            }
        }
        return formatExpression;
    },
    percentage: (formatExpression) => `${formatExpression}%`,
    suffix: (formatExpression, format) => {
        if (format.suffix) {
            return `${formatExpression}"${format.suffix}"`;
        }
        return formatExpression;
    },
};
function convertCustomFormatToFormatExpression(format) {
    // ECMA-376 format expression
    let defaultFormatExpression = null;
    let conversions = [];
    switch (format.type) {
        case field_1.CustomFormatType.CUSTOM: {
            // no conversion needed
            return format.custom || null;
        }
        case field_1.CustomFormatType.DEFAULT: {
            // No format expression needed
            break;
        }
        case field_1.CustomFormatType.CURRENCY: {
            defaultFormatExpression = '0';
            conversions = ['separator', 'currency', 'round', 'compact'];
            break;
        }
        case field_1.CustomFormatType.PERCENT: {
            defaultFormatExpression = '0';
            conversions = ['separator', 'round', 'percentage'];
            break;
        }
        case field_1.CustomFormatType.NUMBER: {
            defaultFormatExpression = `0`;
            conversions = ['separator', 'prefix', 'round', 'compact', 'suffix'];
            break;
        }
        case field_1.CustomFormatType.BYTES_SI: {
            defaultFormatExpression = '0';
            conversions = ['separator', 'round', 'compact'];
            break;
        }
        case field_1.CustomFormatType.BYTES_IEC: {
            // ECMA-376 format expressions cannot accurately represent binary (1024-based) scaling
            // Use a special format that includes the suffix but no comma scaling
            defaultFormatExpression = '0';
            conversions = ['separator', 'round', 'compact'];
            break;
        }
        case field_1.CustomFormatType.ID:
        case field_1.CustomFormatType.DATE:
        case field_1.CustomFormatType.TIMESTAMP: {
            // No format expression needed
            break;
        }
        default: {
            return (0, assertUnreachable_1.default)(format.type, `Cannot recognise ${format.type} format type`);
        }
    }
    if (defaultFormatExpression === null) {
        return defaultFormatExpression;
    }
    // Apply conversions
    return conversions.reduce((expression, fnKey) => customFormatConversionFnMap[fnKey](expression, format), defaultFormatExpression);
}
function getFormatExpression(item) {
    if (hasValidFormatExpression(item)) {
        return item.format;
    }
    const customFormat = getCustomFormat(item);
    return customFormat
        ? convertCustomFormatToFormatExpression(customFormat) || undefined
        : undefined;
}
function getExcelFormatExpression(item) {
    const formatExpression = getFormatExpression(item);
    if (formatExpression === '#,##0.###' &&
        (0, field_1.isMetric)(item) &&
        [field_1.MetricType.COUNT, field_1.MetricType.COUNT_DISTINCT].includes(item.type) &&
        !hasFormatting(item)) {
        return '#,##0';
    }
    return formatExpression;
}
function formatItemValue(item, value, convertToUTC, parameters, timezone, displayTimezone) {
    if (value === null)
        return '∅';
    if (value === undefined)
        return '-';
    if (item) {
        // One timezone decision for every temporal branch below.
        const effectiveTimezone = (0, exports.getFormatterTimezone)(item, value, timezone);
        if (hasValidFormatExpression(item)) {
            // A field-level separator localises the ECMA-376 expression, which
            // numfmt otherwise renders with US separators regardless of locale.
            const separatorLocale = separatorToNumfmtLocale(getEffectiveSeparator(item));
            // Check if format uses parameter placeholders
            const hasParameterPlaceholders = item.format.includes(`\${${parameters_1.LightdashParameters.PREFIX_SHORT}`) || item.format.includes(`\${${parameters_1.LightdashParameters.PREFIX}`);
            // NEW: Handle parameter-based formats separately
            if (hasParameterPlaceholders) {
                // If parameters are provided, evaluate and apply the format
                if (parameters) {
                    const formatExpression = (0, conditionalFormatExpressions_1.evaluateConditionalFormatExpression)(item.format, parameters);
                    try {
                        const result = formatValueWithExpression(formatExpression, value, separatorLocale, effectiveTimezone);
                        return result;
                    }
                    catch (error) {
                        // If evaluation fails, fall back to default formatting
                        return applyDefaultFormat(value);
                    }
                }
                else {
                    // No parameters provided but format needs them - use default formatting
                    return applyDefaultFormat(value);
                }
            }
            // EXISTING: Handle non-parameter formats (unchanged behavior)
            try {
                const result = formatValueWithExpression(item.format, value, separatorLocale, effectiveTimezone);
                return result;
            }
            catch (error) {
                // Fall through to custom format handling below
            }
        }
        const customFormat = getCustomFormat(item);
        if ((0, field_1.isCustomSqlDimension)(item) || 'type' in item) {
            const type = (0, item_1.getItemType)(item);
            // Date/Timestamp table calculations may carry a CUSTOM format
            // expression (e.g. "mmmm d, yyyy"). The default switch path
            // hardwires formatDate/formatTimestamp and would silently drop
            // it, so honour the custom expression first.
            if ((type === field_1.TableCalculationType.DATE ||
                type === field_1.TableCalculationType.TIMESTAMP) &&
                customFormat?.type === field_1.CustomFormatType.CUSTOM &&
                customFormat.custom &&
                (0, exports.isMomentInput)(value)) {
                try {
                    return formatValueWithExpression(customFormat.custom, value, separatorToNumfmtLocale(customFormat.separator), effectiveTimezone);
                }
                catch {
                    // Fall through to the default date/timestamp render.
                }
            }
            switch (type) {
                case field_1.TableCalculationType.STRING:
                case field_1.DimensionType.STRING:
                case field_1.MetricType.STRING:
                    return `${value}`;
                case field_1.DimensionType.BOOLEAN:
                case field_1.MetricType.BOOLEAN:
                case field_1.TableCalculationType.BOOLEAN:
                    return (0, exports.formatBoolean)(value);
                case field_1.DimensionType.DATE:
                case field_1.MetricType.DATE:
                case field_1.TableCalculationType.DATE: {
                    // getFormatterTimezone returns undefined for calendar dates,
                    // so effectiveTimezone is safe to pass here.
                    return (0, exports.isMomentInput)(value)
                        ? formatDate(value, (0, field_1.isDimension)(item) ? item.timeInterval : undefined, convertToUTC, effectiveTimezone)
                        : 'NaT';
                }
                case field_1.DimensionType.TIMESTAMP:
                case field_1.MetricType.TIMESTAMP:
                case field_1.TableCalculationType.TIMESTAMP:
                    return (0, exports.isMomentInput)(value)
                        ? formatTimestamp(value, (0, field_1.isDimension)(item) ? item.timeInterval : undefined, convertToUTC, effectiveTimezone, displayTimezone)
                        : 'NaT';
                case field_1.MetricType.MAX:
                case field_1.MetricType.MIN: {
                    // A temporal MIN/MAX shifts like a dimension; a user-chosen
                    // display format wins and falls through to applyCustomFormat.
                    const formatType = customFormat?.type;
                    const userChoseDisplayFormat = formatType === field_1.CustomFormatType.DATE ||
                        formatType === field_1.CustomFormatType.TIMESTAMP ||
                        formatType === field_1.CustomFormatType.CUSTOM;
                    if ((0, exports.isTemporalValue)(value) && !userChoseDisplayFormat) {
                        return formatTimestamp(value, undefined, convertToUTC, effectiveTimezone, displayTimezone);
                    }
                    break;
                }
                case field_1.DimensionType.NUMBER:
                    if ((0, field_1.isDimension)(item) &&
                        item.timeInterval &&
                        item.timeInterval === timeFrames_1.TimeFrames.YEAR_NUM // Year number (e.g. 2021) is a number, but should be formatted as a string so there's no separator applied
                    ) {
                        return `${value}`;
                    }
                    break;
                default:
            }
        }
        return applyCustomFormat(value, customFormat, effectiveTimezone);
    }
    return applyDefaultFormat(value);
}
//# sourceMappingURL=formatting.js.map