"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseTimestamp = exports.parseDate = exports.isMomentInput = exports.getDateFormat = exports.formatBoolean = exports.currencies = void 0;
exports.formatDate = formatDate;
exports.formatTimestamp = formatTimestamp;
exports.getLocalTimeDisplay = getLocalTimeDisplay;
exports.valueIsNaN = valueIsNaN;
exports.isNumber = isNumber;
exports.formatNumberValue = formatNumberValue;
exports.applyDefaultFormat = applyDefaultFormat;
exports.getCustomFormatFromLegacy = getCustomFormatFromLegacy;
exports.hasFormatting = hasFormatting;
exports.getCustomFormat = getCustomFormat;
exports.formatValueWithExpression = formatValueWithExpression;
exports.applyCustomFormat = applyCustomFormat;
exports.hasValidFormatExpression = hasValidFormatExpression;
exports.convertCustomFormatToFormatExpression = convertCustomFormatToFormatExpression;
exports.getFormatExpression = getFormatExpression;
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_1 = tslib_1.__importDefault(require("moment"));
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_1.default ||
    value instanceof dayjs_1.default;
exports.isMomentInput = isMomentInput;
function formatDate(date, timeInterval = timeFrames_1.TimeFrames.DAY, convertToUTC = false) {
    const momentDate = convertToUTC ? (0, moment_1.default)(date).utc() : (0, moment_1.default)(date);
    if (!momentDate.isValid()) {
        return 'NaT';
    }
    return momentDate.format((0, exports.getDateFormat)(timeInterval));
}
function formatTimestamp(value, timeInterval = timeFrames_1.TimeFrames.MILLISECOND, convertToUTC = false) {
    const momentDate = convertToUTC ? (0, moment_1.default)(value).utc() : (0, moment_1.default)(value);
    if (!momentDate.isValid()) {
        return 'NaT';
    }
    return momentDate.format(getTimeFormat(timeInterval));
}
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_1.default)(value).format(`YYYY-MM-DD HH:mm`)} ${tzString}`;
}
const parseDate = (str, timeInterval = timeFrames_1.TimeFrames.DAY) => (0, moment_1.default)(str, (0, exports.getDateFormat)(timeInterval)).toDate();
exports.parseDate = parseDate;
const parseTimestamp = (str, timeInterval = timeFrames_1.TimeFrames.MILLISECOND) => (0, moment_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);
}
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.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;
}
function getCustomFormat(item) {
    if (!item)
        return undefined;
    if ((0, metricQuery_1.hasFormatOptions)(item)) {
        return item.formatOptions;
    }
    if ((0, field_1.isTableCalculation)(item)) {
        return item.format;
    }
    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)) {
        return getCustomFormatFromLegacy(legacyFormat);
    }
    return undefined;
}
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) {
    try {
        let sanitizedValue = value;
        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);
                return `${formattedNumber}${binarySuffixMatch}`;
            }
        }
        // format date
        if ((0, numfmt_1.isDateFormat)(expression)) {
            if (!(0, exports.isMomentInput)(sanitizedValue)) {
                return 'NaT';
            }
            return (0, numfmt_1.format)(expression, (0, moment_1.default)(sanitizedValue).toDate(), { 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));
    }
    catch (e) {
        // eslint-disable-next-line no-console
        console.error('Error formatting value with expression', e);
        return `${value}`;
    }
}
function applyCustomFormat(value, format) {
    if (format?.type === undefined)
        return applyDefaultFormat(value);
    if (value === '')
        return '';
    if (value instanceof Date &&
        ![field_1.CustomFormatType.DATE, field_1.CustomFormatType.TIMESTAMP].includes(format.type)) {
        return formatTimestamp(value, undefined, false);
    }
    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);
        case field_1.CustomFormatType.TIMESTAMP:
            return formatTimestamp(value, format?.timeInterval, false);
        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);
        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 formatItemValue(item, value, convertToUTC, parameters) {
    if (value === null)
        return '∅';
    if (value === undefined)
        return '-';
    if (item) {
        if (hasValidFormatExpression(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);
                        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);
                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);
            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:
                    return (0, exports.isMomentInput)(value)
                        ? formatDate(value, (0, field_1.isDimension)(item) ? item.timeInterval : undefined, convertToUTC)
                        : '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)
                        : 'NaT';
                case field_1.MetricType.MAX:
                case field_1.MetricType.MIN:
                    if (value instanceof Date && customFormat === undefined) {
                        return formatTimestamp(value, (0, field_1.isDimension)(item) ? item.timeInterval : undefined, convertToUTC);
                    }
                    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);
    }
    return applyDefaultFormat(value);
}
//# sourceMappingURL=formatting.js.map