"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDateDimension = exports.isSubDayGranularity = exports.SUB_DAY_GRANULARITIES = exports.sortTimeFrames = exports.timeFrameOrder = exports.validateTimeFrames = exports.isTimeInterval = exports.extractableTimeFrames = exports.truncatableTimeFrames = exports.getTimeFramesWithProjectDefaults = exports.getDefaultTimeFrames = exports.timeFrameConfigs = exports.getSqlForDatePartName = exports.getSqlForDatePart = exports.getSqlForTruncatedDate = exports.isTimezoneRoundTripNoOp = exports.isSubDayTimeFrame = exports.SUB_DAY_TIME_FRAMES = exports.dateExtractsTimezoneConversions = exports.dateTruncTimezoneConversions = exports.isWeekDay = exports.WeekDay = void 0;
const dbt_1 = require("../types/dbt");
const errors_1 = require("../types/errors");
const field_1 = require("../types/field");
const timeFrames_1 = require("../types/timeFrames");
var WeekDay;
(function (WeekDay) {
    WeekDay[WeekDay["MONDAY"] = 0] = "MONDAY";
    WeekDay[WeekDay["TUESDAY"] = 1] = "TUESDAY";
    WeekDay[WeekDay["WEDNESDAY"] = 2] = "WEDNESDAY";
    WeekDay[WeekDay["THURSDAY"] = 3] = "THURSDAY";
    WeekDay[WeekDay["FRIDAY"] = 4] = "FRIDAY";
    WeekDay[WeekDay["SATURDAY"] = 5] = "SATURDAY";
    WeekDay[WeekDay["SUNDAY"] = 6] = "SUNDAY";
})(WeekDay || (exports.WeekDay = WeekDay = {}));
const isWeekDay = (value) => Number.isSafeInteger(value) &&
    value >= 0 &&
    value <= 6;
exports.isWeekDay = isWeekDay;
const nullTimeFrameMap = {
    DAY: null,
    DAY_OF_MONTH_NUM: null,
    DAY_OF_WEEK_INDEX: null,
    DAY_OF_WEEK_NAME: null,
    DAY_OF_YEAR_NUM: null,
    HOUR: null,
    MILLISECOND: null,
    MINUTE: null,
    MONTH: null,
    MONTH_NAME: null,
    MONTH_NUM: null,
    QUARTER: null,
    QUARTER_NAME: null,
    QUARTER_NUM: null,
    RAW: null,
    SECOND: null,
    WEEK: null,
    WEEK_NUM: null,
    YEAR: null,
    YEAR_NUM: null,
    HOUR_OF_DAY_NUM: null,
    MINUTE_OF_HOUR_NUM: null,
};
const timeFrameToDatePartMap = {
    ...nullTimeFrameMap,
    [timeFrames_1.TimeFrames.DAY_OF_WEEK_INDEX]: 'DOW',
    [timeFrames_1.TimeFrames.DAY_OF_MONTH_NUM]: 'DAY',
    [timeFrames_1.TimeFrames.DAY_OF_YEAR_NUM]: 'DOY',
    [timeFrames_1.TimeFrames.WEEK_NUM]: 'WEEK',
    [timeFrames_1.TimeFrames.MONTH_NUM]: 'MONTH',
    [timeFrames_1.TimeFrames.QUARTER_NUM]: 'QUARTER',
    [timeFrames_1.TimeFrames.YEAR_NUM]: 'YEAR',
    [timeFrames_1.TimeFrames.HOUR_OF_DAY_NUM]: 'HOUR',
    [timeFrames_1.TimeFrames.MINUTE_OF_HOUR_NUM]: 'MINUTE',
};
const bigqueryCastToInstant = (sql) => `TIMESTAMP(${sql})`;
const postgresLikeCastToInstant = (sql) => `(${sql})::timestamptz`;
// ClickHouse DateTime values are instants; session_timezone only changes how
// they serialize. Pinning to UTC keeps the wire value a parseable instant.
const clickhouseCastToInstant = (sql) => `toTimeZone(${sql}, 'UTC')`;
const identityCastToInstant = (sql) => sql;
exports.dateTruncTimezoneConversions = {
    // BigQuery: shift the UTC instant into a naive project-TZ DATETIME so the
    // DST fall-back fold collapses under DATETIME_TRUNC (merge), then re-attach
    // the zone to recover a real instant. Coerce via TIMESTAMP() first because a
    // declared-TIMESTAMP dim can emit DATETIME at runtime; DATETIME(timestamp,
    // tz) requires a TIMESTAMP left side.
    [dbt_1.SupportedDbtAdapter.BIGQUERY]: {
        toProjectTz: (sql, tz) => `DATETIME(${bigqueryCastToInstant(sql)}, '${tz}')`,
        toUTC: (sql, tz) => `TIMESTAMP(${sql}, '${tz}')`,
        castAsDate: (sql) => `CAST(${sql} AS DATE)`,
        castToInstant: bigqueryCastToInstant,
    },
    [dbt_1.SupportedDbtAdapter.SNOWFLAKE]: {
        toProjectTz: (sql, tz, sourceTimezone = 'UTC') => `CONVERT_TIMEZONE('${sourceTimezone}', '${tz}', ${sql})`,
        toUTC: (sql, tz) => `CONVERT_TIMEZONE('${tz}', 'UTC', ${sql})`,
        castAsDate: (sql) => `CAST(${sql} AS DATE)`,
        castToInstant: identityCastToInstant,
    },
    [dbt_1.SupportedDbtAdapter.POSTGRES]: {
        toProjectTz: (sql, tz) => `${postgresLikeCastToInstant(sql)} AT TIME ZONE '${tz}'`,
        toUTC: (sql, tz) => `(${sql}) AT TIME ZONE '${tz}'`,
        castAsDate: (sql) => `CAST(${sql} AS DATE)`,
        castToInstant: postgresLikeCastToInstant,
    },
    [dbt_1.SupportedDbtAdapter.REDSHIFT]: {
        toProjectTz: (sql, tz) => `${postgresLikeCastToInstant(sql)} AT TIME ZONE '${tz}'`,
        toUTC: (sql, tz) => `(${sql}) AT TIME ZONE '${tz}'`,
        castAsDate: (sql) => `CAST(${sql} AS DATE)`,
        castToInstant: postgresLikeCastToInstant,
    },
    [dbt_1.SupportedDbtAdapter.DUCKDB]: {
        toProjectTz: (sql, tz) => `${postgresLikeCastToInstant(sql)} AT TIME ZONE '${tz}'`,
        toUTC: (sql, tz) => `(${sql}) AT TIME ZONE '${tz}'`,
        castAsDate: (sql) => `CAST(${sql} AS DATE)`,
        castToInstant: postgresLikeCastToInstant,
    },
    // Databricks TIMESTAMP is an instant (LTZ-like), so a session-tz rebase
    // would double-shift it — castToInstant stays identity.
    [dbt_1.SupportedDbtAdapter.DATABRICKS]: {
        toProjectTz: (sql, tz) => `from_utc_timestamp(to_utc_timestamp(${sql}, current_timezone()), '${tz}')`,
        toUTC: (sql, tz) => `to_utc_timestamp(${sql}, '${tz}')`,
        castAsDate: (sql) => `CAST(${sql} AS DATE)`,
        castToInstant: identityCastToInstant,
    },
    [dbt_1.SupportedDbtAdapter.SPARK]: {
        toProjectTz: (sql, tz) => `from_utc_timestamp(to_utc_timestamp(${sql}, current_timezone()), '${tz}')`,
        toUTC: (sql, tz) => `to_utc_timestamp(${sql}, '${tz}')`,
        castAsDate: (sql) => `CAST(${sql} AS DATE)`,
        castToInstant: identityCastToInstant,
    },
    // Trino returns `timestamp with time zone` values as strings like
    // "2024-01-14 00:00:00.000 America/New_York", which dayjs/moment can't
    // parse — so `toUTC` casts the UTC-shifted result back to a naive
    // `timestamp`. `with_timezone` attaches the project zone explicitly
    // (independent of session zone) before shifting to UTC.
    [dbt_1.SupportedDbtAdapter.TRINO]: {
        toProjectTz: (sql, tz) => `CAST(${sql} AT TIME ZONE '${tz}' AS timestamp)`,
        toUTC: (sql, tz) => `CAST(with_timezone(${sql}, '${tz}') AT TIME ZONE 'UTC' AS timestamp)`,
        castAsDate: (sql) => `CAST(${sql} AS DATE)`,
        castToInstant: identityCastToInstant,
    },
    [dbt_1.SupportedDbtAdapter.ATHENA]: {
        toProjectTz: (sql, tz) => `CAST(${sql} AT TIME ZONE '${tz}' AS timestamp)`,
        toUTC: (sql, tz) => `CAST(with_timezone(${sql}, '${tz}') AT TIME ZONE 'UTC' AS timestamp)`,
        castAsDate: (sql) => `CAST(${sql} AS DATE)`,
        castToInstant: identityCastToInstant,
    },
    // Merge the DST fall-back: ClickHouse DateTime always carries a zone and
    // toTimeZone only relabels (instant domain), so the two folded instants
    // would stay distinct. Render project-TZ wall-clock to a string and
    // re-parse as naive UTC so the fold collapses before truncation; toUTC
    // re-parses the truncated wall-clock as project-local to recover a real
    // instant. The minute specifier is %i (%M is month name); %f keeps
    // sub-second precision so MILLISECOND-grain data is not rounded to seconds.
    [dbt_1.SupportedDbtAdapter.CLICKHOUSE]: {
        toProjectTz: (sql, tz) => `toDateTime64(formatDateTime(toTimeZone(${sql}, '${tz}'), '%Y-%m-%d %H:%i:%S.%f'), 3, 'UTC')`,
        toUTC: (sql, tz) => `toTimeZone(toDateTime64(formatDateTime(${sql}, '%Y-%m-%d %H:%i:%S.%f'), 3, '${tz}'), 'UTC')`,
        castAsDate: (sql) => `CAST(${sql} AS DATE)`,
        castToInstant: clickhouseCastToInstant,
    },
};
exports.dateExtractsTimezoneConversions = {
    // `AT TIME ZONE` parses only inside `EXTRACT(... FROM ...)` and requires
    // TIMESTAMP, so coerce DATETIME-shaped inputs.
    [dbt_1.SupportedDbtAdapter.BIGQUERY]: {
        toExtractInputTz: (sql, tz) => `TIMESTAMP(${sql}) AT TIME ZONE '${tz}'`,
    },
    [dbt_1.SupportedDbtAdapter.SNOWFLAKE]: {
        toExtractInputTz: (sql, tz, sourceTimezone = 'UTC') => `CONVERT_TIMEZONE('${sourceTimezone}', '${tz}', ${sql})`,
    },
    [dbt_1.SupportedDbtAdapter.POSTGRES]: {
        toExtractInputTz: (sql, tz) => `(${sql})::timestamptz AT TIME ZONE '${tz}'`,
    },
    [dbt_1.SupportedDbtAdapter.REDSHIFT]: {
        toExtractInputTz: (sql, tz) => `(${sql})::timestamptz AT TIME ZONE '${tz}'`,
    },
    [dbt_1.SupportedDbtAdapter.DUCKDB]: {
        toExtractInputTz: (sql, tz) => `(${sql})::timestamptz AT TIME ZONE '${tz}'`,
    },
    [dbt_1.SupportedDbtAdapter.DATABRICKS]: {
        toExtractInputTz: (sql, tz) => `from_utc_timestamp(to_utc_timestamp(${sql}, current_timezone()), '${tz}')`,
    },
    [dbt_1.SupportedDbtAdapter.SPARK]: {
        toExtractInputTz: (sql, tz) => `from_utc_timestamp(to_utc_timestamp(${sql}, current_timezone()), '${tz}')`,
    },
    [dbt_1.SupportedDbtAdapter.TRINO]: {
        toExtractInputTz: (sql, tz) => `CAST(${sql} AT TIME ZONE '${tz}' AS timestamp)`,
    },
    [dbt_1.SupportedDbtAdapter.ATHENA]: {
        toExtractInputTz: (sql, tz) => `CAST(${sql} AT TIME ZONE '${tz}' AS timestamp)`,
    },
    [dbt_1.SupportedDbtAdapter.CLICKHOUSE]: {
        toExtractInputTz: (sql, tz) => `toTimeZone(${sql}, '${tz}')`,
    },
};
exports.SUB_DAY_TIME_FRAMES = new Set([
    timeFrames_1.TimeFrames.MILLISECOND,
    timeFrames_1.TimeFrames.SECOND,
    timeFrames_1.TimeFrames.MINUTE,
    timeFrames_1.TimeFrames.HOUR,
]);
const isSubDayTimeFrame = (tf) => exports.SUB_DAY_TIME_FRAMES.has(tf);
exports.isSubDayTimeFrame = isSubDayTimeFrame;
const bigqueryStartOfWeekMap = {
    [WeekDay.MONDAY]: 'MONDAY',
    [WeekDay.TUESDAY]: 'TUESDAY',
    [WeekDay.WEDNESDAY]: 'WEDNESDAY',
    [WeekDay.THURSDAY]: 'THURSDAY',
    [WeekDay.FRIDAY]: 'FRIDAY',
    [WeekDay.SATURDAY]: 'SATURDAY',
    [WeekDay.SUNDAY]: 'SUNDAY',
};
const bigqueryConfig = {
    // BigQuery: on the timezone-wrapped path `toProjectTz` has already shifted
    // the value into a naive project-TZ DATETIME, so truncate with
    // DATETIME_TRUNC (the fold merges) and let `toUTC` re-attach the zone. The
    // bare path (no timezone) keeps TIMESTAMP_TRUNC, truncating the UTC instant.
    getSqlForTruncatedDate: (timeFrame, originalSql, type, startOfWeek, timezone) => {
        const datePart = timeFrame === timeFrames_1.TimeFrames.WEEK && (0, exports.isWeekDay)(startOfWeek)
            ? `${timeFrame}(${bigqueryStartOfWeekMap[startOfWeek]})`
            : timeFrame;
        if (type === field_1.DimensionType.TIMESTAMP) {
            if (timezone) {
                return `DATETIME_TRUNC(${originalSql}, ${datePart})`;
            }
            return `TIMESTAMP_TRUNC(${originalSql}, ${datePart})`;
        }
        return `DATE_TRUNC(${originalSql}, ${datePart})`;
    },
    getSqlForDatePart: (timeFrame, originalSql, _, startOfWeek) => {
        const bigqueryTimeFrameExpressions = {
            ...timeFrameToDatePartMap,
            [timeFrames_1.TimeFrames.DAY_OF_WEEK_INDEX]: 'DAYOFWEEK',
            [timeFrames_1.TimeFrames.DAY_OF_YEAR_NUM]: 'DAYOFYEAR',
        };
        const datePart = bigqueryTimeFrameExpressions[timeFrame];
        if (!datePart) {
            throw new errors_1.ParseError(`Cannot recognise date part for ${timeFrame}`);
        }
        if (timeFrame === timeFrames_1.TimeFrames.WEEK_NUM && (0, exports.isWeekDay)(startOfWeek)) {
            return `EXTRACT(${datePart}(${bigqueryStartOfWeekMap[startOfWeek]}) FROM ${originalSql})`;
        }
        // BigQuery DAYOFWEEK: 1=Sunday, 2=Monday, ..., 7=Saturday
        if (timeFrame === timeFrames_1.TimeFrames.DAY_OF_WEEK_INDEX &&
            (0, exports.isWeekDay)(startOfWeek)) {
            const nativeOffset = ((startOfWeek + 1) % 7) + 1;
            return `MOD(EXTRACT(DAYOFWEEK FROM ${originalSql}) - ${nativeOffset} + 7, 7) + 1`;
        }
        return `EXTRACT(${datePart} FROM ${originalSql})`;
    },
    getSqlForDatePartName: (timeFrame, originalSql, type, timezone) => {
        // https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#format_elements_date_time
        const timeFrameExpressions = {
            ...nullTimeFrameMap,
            [timeFrames_1.TimeFrames.DAY_OF_WEEK_NAME]: '%A',
            [timeFrames_1.TimeFrames.MONTH_NAME]: '%B',
            [timeFrames_1.TimeFrames.QUARTER_NAME]: 'Q%Q',
        };
        const formatExpression = timeFrameExpressions[timeFrame];
        if (!formatExpression) {
            throw new errors_1.ParseError(`Cannot recognise format expression for ${timeFrame}`);
        }
        if (type === field_1.DimensionType.TIMESTAMP) {
            if (timezone) {
                // FORMAT_TIMESTAMP requires TIMESTAMP; coerce DATETIME-shaped inputs.
                return `FORMAT_TIMESTAMP('${formatExpression}', TIMESTAMP(${originalSql}), '${timezone}')`;
            }
            return `FORMAT_DATETIME('${formatExpression}', ${originalSql})`;
        }
        return `FORMAT_DATE('${formatExpression}', ${originalSql})`;
    },
};
// Snowflake handles start of week by setting a session variable (WEEK_START)
const snowflakeConfig = {
    getSqlForTruncatedDate: (timeFrame, originalSql) => `DATE_TRUNC('${timeFrame}', ${originalSql})`,
    getSqlForDatePart: (timeFrame, originalSql) => {
        const datePart = timeFrameToDatePartMap[timeFrame];
        if (!datePart) {
            throw new errors_1.ParseError(`Cannot recognise date part for ${timeFrame}`);
        }
        return `DATE_PART('${datePart}', ${originalSql})`;
    },
    getSqlForDatePartName: (timeFrame, originalSql, _type, timezone, sourceTimezone) => {
        const sql = timezone
            ? exports.dateExtractsTimezoneConversions[dbt_1.SupportedDbtAdapter.SNOWFLAKE].toExtractInputTz(originalSql, timezone, sourceTimezone)
            : originalSql;
        // https://docs.snowflake.com/en/sql-reference/functions/to_char.html
        const timeFrameExpressionsFn = {
            ...nullTimeFrameMap,
            [timeFrames_1.TimeFrames.DAY_OF_WEEK_NAME]: () => `DECODE(TO_CHAR(${sql}, 'DY'), 'Mon', 'Monday', 'Tue', 'Tuesday', 'Wed', 'Wednesday', 'Thu', 'Thursday', 'Fri', 'Friday', 'Sat', 'Saturday', 'Sun', 'Sunday')`,
            [timeFrames_1.TimeFrames.MONTH_NAME]: () => `TO_CHAR(${sql}, 'MMMM')`,
            [timeFrames_1.TimeFrames.QUARTER_NAME]: () => `CONCAT('Q', DATE_PART('QUARTER', ${sql}))`,
        };
        const formatExpressionFn = timeFrameExpressionsFn[timeFrame];
        if (!formatExpressionFn) {
            throw new errors_1.ParseError(`Cannot recognise format expression for ${timeFrame}`);
        }
        return formatExpressionFn();
    },
};
const postgresConfig = {
    getSqlForTruncatedDate: (timeFrame, originalSql, _, startOfWeek) => {
        if (timeFrame === timeFrames_1.TimeFrames.WEEK && (0, exports.isWeekDay)(startOfWeek)) {
            const intervalDiff = `${startOfWeek} days`;
            return `(DATE_TRUNC('${timeFrame}', (${originalSql} - interval '${intervalDiff}')) + interval '${intervalDiff}')`;
        }
        return `DATE_TRUNC('${timeFrame}', ${originalSql})`;
    },
    getSqlForDatePart: (timeFrame, originalSql, _, startOfWeek) => {
        const datePart = timeFrameToDatePartMap[timeFrame];
        if (!datePart) {
            throw new errors_1.ParseError(`Cannot recognise date part for ${timeFrame}`);
        }
        if (timeFrame === timeFrames_1.TimeFrames.WEEK_NUM && (0, exports.isWeekDay)(startOfWeek)) {
            const intervalDiff = `${startOfWeek} days`;
            return `DATE_PART('${datePart}', (${originalSql} - interval '${intervalDiff}'))`;
        }
        // PostgreSQL DOW: 0=Sunday, 1=Monday, ..., 6=Saturday
        if (timeFrame === timeFrames_1.TimeFrames.DAY_OF_WEEK_INDEX &&
            (0, exports.isWeekDay)(startOfWeek)) {
            const nativeOffset = (startOfWeek + 1) % 7;
            return `MOD(CAST(DATE_PART('DOW', ${originalSql}) AS INT) - ${nativeOffset} + 7, 7) + 1`;
        }
        return `DATE_PART('${datePart}', ${originalSql})`;
    },
    getSqlForDatePartName: (timeFrame, originalSql, _type, timezone) => {
        const sql = timezone
            ? exports.dateExtractsTimezoneConversions[dbt_1.SupportedDbtAdapter.POSTGRES].toExtractInputTz(originalSql, timezone)
            : originalSql;
        // https://www.postgresql.org/docs/current/functions-formatting.html
        const timeFrameExpressions = {
            ...nullTimeFrameMap,
            [timeFrames_1.TimeFrames.DAY_OF_WEEK_NAME]: 'Day',
            [timeFrames_1.TimeFrames.MONTH_NAME]: 'Month',
            [timeFrames_1.TimeFrames.QUARTER_NAME]: '"Q"Q',
        };
        const formatExpression = timeFrameExpressions[timeFrame];
        if (!formatExpression) {
            throw new errors_1.ParseError(`Cannot recognise format expression for ${timeFrame}`);
        }
        return `TO_CHAR(${sql}, 'FM${formatExpression}')`;
    },
};
const databricksConfig = {
    getSqlForTruncatedDate: (timeFrame, originalSql, _, startOfWeek) => {
        if (timeFrame === timeFrames_1.TimeFrames.WEEK && (0, exports.isWeekDay)(startOfWeek)) {
            const intervalDiff = `${startOfWeek}`;
            return `DATEADD(DAY, ${intervalDiff}, DATE_TRUNC('${timeFrame}', DATEADD(DAY, -${intervalDiff}, ${originalSql})))`;
        }
        return `DATE_TRUNC('${timeFrame}', ${originalSql})`;
    },
    getSqlForDatePart: (timeFrame, originalSql, _, startOfWeek) => {
        const datePart = timeFrameToDatePartMap[timeFrame];
        if (!datePart) {
            throw new errors_1.ParseError(`Cannot recognise date part for ${timeFrame}`);
        }
        if (timeFrame === timeFrames_1.TimeFrames.WEEK_NUM && (0, exports.isWeekDay)(startOfWeek)) {
            const intervalDiff = `${startOfWeek} days`;
            return `DATE_PART('${datePart}', (${originalSql} - interval '${intervalDiff}'))`;
        }
        // Databricks DOW: 0=Sunday, 1=Monday, ..., 6=Saturday
        if (timeFrame === timeFrames_1.TimeFrames.DAY_OF_WEEK_INDEX &&
            (0, exports.isWeekDay)(startOfWeek)) {
            const nativeOffset = (startOfWeek + 1) % 7;
            return `MOD(CAST(DATE_PART('DOW', ${originalSql}) AS INT) - ${nativeOffset} + 7, 7) + 1`;
        }
        return `DATE_PART('${datePart}', ${originalSql})`;
    },
    getSqlForDatePartName: (timeFrame, originalSql, _type, timezone) => {
        const sql = timezone
            ? exports.dateExtractsTimezoneConversions[dbt_1.SupportedDbtAdapter.DATABRICKS].toExtractInputTz(originalSql, timezone)
            : originalSql;
        // https://docs.databricks.com/spark/latest/spark-sql/language-manual/functions/date_format.html
        const timeFrameExpressions = {
            ...nullTimeFrameMap,
            [timeFrames_1.TimeFrames.DAY_OF_WEEK_NAME]: 'EEEE',
            [timeFrames_1.TimeFrames.MONTH_NAME]: 'MMMM',
            [timeFrames_1.TimeFrames.QUARTER_NAME]: 'QQQ',
        };
        const formatExpression = timeFrameExpressions[timeFrame];
        if (!formatExpression) {
            throw new errors_1.ParseError(`Cannot recognise format expression for ${timeFrame}`);
        }
        return `DATE_FORMAT(${sql}, '${formatExpression}')`;
    },
};
const trinoConfig = {
    // Trino rejects sub-day DATE_TRUNC on DATE columns ('SECOND' is not a
    // valid DATE field). Cast to TIMESTAMP for sub-day grains; no-op when the
    // input is already a TIMESTAMP.
    getSqlForTruncatedDate: (timeFrame, originalSql, _, startOfWeek) => {
        const sql = (0, exports.isSubDayTimeFrame)(timeFrame)
            ? `CAST(${originalSql} AS TIMESTAMP)`
            : originalSql;
        if (timeFrame === timeFrames_1.TimeFrames.WEEK && (0, exports.isWeekDay)(startOfWeek)) {
            const intervalDiff = `'${startOfWeek}' day`;
            return `(DATE_TRUNC('${timeFrame}', (${sql} - interval ${intervalDiff})) + interval ${intervalDiff})`;
        }
        return `DATE_TRUNC('${timeFrame}', ${sql})`;
    },
    getSqlForDatePart: (timeFrame, originalSql, _, startOfWeek) => {
        const datePart = timeFrameToDatePartMap[timeFrame];
        if (!datePart) {
            throw new errors_1.ParseError(`Cannot recognise date part for ${timeFrame}`);
        }
        if (timeFrame === timeFrames_1.TimeFrames.WEEK_NUM && (0, exports.isWeekDay)(startOfWeek)) {
            const intervalDiff = `'${startOfWeek}' day`;
            return `EXTRACT(${datePart} FROM (${originalSql} - interval ${intervalDiff}))`;
        }
        // Trino DOW (ISO): 1=Monday, 2=Tuesday, ..., 7=Sunday
        if (timeFrame === timeFrames_1.TimeFrames.DAY_OF_WEEK_INDEX &&
            (0, exports.isWeekDay)(startOfWeek)) {
            const nativeOffset = startOfWeek + 1;
            return `MOD(EXTRACT(DOW FROM ${originalSql}) - ${nativeOffset} + 7, 7) + 1`;
        }
        return `EXTRACT(${datePart} FROM ${originalSql})`;
    },
    getSqlForDatePartName: (timeFrame, originalSql, _type, timezone) => {
        const sql = timezone
            ? exports.dateExtractsTimezoneConversions[dbt_1.SupportedDbtAdapter.TRINO].toExtractInputTz(originalSql, timezone)
            : originalSql;
        const timeFrameExpressionsFn = {
            ...nullTimeFrameMap,
            [timeFrames_1.TimeFrames.DAY_OF_WEEK_NAME]: () => `date_format(${sql}, '%W')`,
            [timeFrames_1.TimeFrames.MONTH_NAME]: () => `date_format(${sql}, '%M')`,
            [timeFrames_1.TimeFrames.QUARTER_NAME]: () => `CONCAT('Q', cast(extract(QUARTER from ${sql}) as varchar))`,
        };
        const formatExpressionFn = timeFrameExpressionsFn[timeFrame];
        if (!formatExpressionFn) {
            throw new errors_1.ParseError(`Cannot recognise format expression for ${timeFrame}`);
        }
        return formatExpressionFn();
    },
};
const clickhouseConfig = {
    getSqlForTruncatedDate: (timeFrame, originalSql, _, startOfWeek) => {
        if (timeFrame === timeFrames_1.TimeFrames.WEEK && (0, exports.isWeekDay)(startOfWeek)) {
            const intervalDiff = startOfWeek;
            // Mode 1 makes toStartOfWeek() return Monday (matching Postgres DATE_TRUNC('week')),
            // so the shift arithmetic is correct for all startOfWeek values.
            return `addDays(toStartOfWeek(addDays(${originalSql}, -${intervalDiff}), 1), ${intervalDiff})`;
        }
        switch (timeFrame) {
            case timeFrames_1.TimeFrames.DAY:
                return `toStartOfDay(${originalSql})`;
            case timeFrames_1.TimeFrames.WEEK:
                return `toStartOfWeek(${originalSql})`;
            case timeFrames_1.TimeFrames.MONTH:
                return `toStartOfMonth(${originalSql})`;
            case timeFrames_1.TimeFrames.QUARTER:
                return `toStartOfQuarter(${originalSql})`;
            case timeFrames_1.TimeFrames.YEAR:
                return `toStartOfYear(${originalSql})`;
            case timeFrames_1.TimeFrames.HOUR:
                return `toStartOfHour(${originalSql})`;
            case timeFrames_1.TimeFrames.MINUTE:
                return `toStartOfMinute(${originalSql})`;
            case timeFrames_1.TimeFrames.SECOND:
                // toStartOfSecond requires DateTime64; lift Date/DateTime via cast.
                return `toStartOfSecond(toDateTime64(${originalSql}, 3))`;
            case timeFrames_1.TimeFrames.MILLISECOND:
                return `toStartOfMillisecond(toDateTime64(${originalSql}, 3))`;
            default:
                throw new errors_1.ParseError(`Cannot recognise truncate function for ${timeFrame}`);
        }
    },
    getSqlForDatePart: (timeFrame, originalSql, _, startOfWeek) => {
        const clickhouseTimeFrameMap = {
            ...timeFrameToDatePartMap,
            [timeFrames_1.TimeFrames.DAY_OF_WEEK_INDEX]: 'toDayOfWeek',
            [timeFrames_1.TimeFrames.DAY_OF_MONTH_NUM]: 'toDayOfMonth',
            [timeFrames_1.TimeFrames.DAY_OF_YEAR_NUM]: 'toDayOfYear',
            [timeFrames_1.TimeFrames.WEEK_NUM]: 'toWeek',
            [timeFrames_1.TimeFrames.MONTH_NUM]: 'toMonth',
            [timeFrames_1.TimeFrames.QUARTER_NUM]: 'toQuarter',
            [timeFrames_1.TimeFrames.YEAR_NUM]: 'toYear',
            [timeFrames_1.TimeFrames.HOUR_OF_DAY_NUM]: 'toHour',
            [timeFrames_1.TimeFrames.MINUTE_OF_HOUR_NUM]: 'toMinute',
        };
        // ClickHouse toDayOfWeek (ISO): 1=Monday, 2=Tuesday, ..., 7=Sunday
        if (timeFrame === timeFrames_1.TimeFrames.DAY_OF_WEEK_INDEX &&
            (0, exports.isWeekDay)(startOfWeek)) {
            const nativeOffset = startOfWeek + 1;
            return `modulo(toDayOfWeek(${originalSql}) - ${nativeOffset} + 7, 7) + 1`;
        }
        // ClickHouse toWeek defaults to mode 0 (US, Sunday-base, 0–53). Mode 3 is
        // ISO 8601 (Monday-base, 1–53), matching Postgres EXTRACT(WEEK) and the
        // toStartOfWeek(d, 1) used by truncation. The startOfWeek shift mirrors
        // what other warehouses do on the same path.
        if (timeFrame === timeFrames_1.TimeFrames.WEEK_NUM && (0, exports.isWeekDay)(startOfWeek)) {
            return `toWeek(addDays(${originalSql}, -${startOfWeek}), 3)`;
        }
        const extractFunction = clickhouseTimeFrameMap[timeFrame];
        if (!extractFunction) {
            throw new errors_1.ParseError(`Cannot recognise extract function for ${timeFrame}`);
        }
        return `${extractFunction}(${originalSql})`;
    },
    getSqlForDatePartName: (timeFrame, originalSql, _type, timezone) => {
        const sql = timezone
            ? exports.dateExtractsTimezoneConversions[dbt_1.SupportedDbtAdapter.CLICKHOUSE].toExtractInputTz(originalSql, timezone)
            : originalSql;
        const timeFrameExpressions = {
            ...nullTimeFrameMap,
            [timeFrames_1.TimeFrames.DAY_OF_WEEK_NAME]: `dateName('weekday', ${sql})`,
            [timeFrames_1.TimeFrames.MONTH_NAME]: `monthName(${sql})`,
            [timeFrames_1.TimeFrames.QUARTER_NAME]: `concat('Q', toString(toQuarter(${sql})))`,
        };
        const formatExpression = timeFrameExpressions[timeFrame];
        if (!formatExpression) {
            throw new errors_1.ParseError(`Cannot recognise format expression for ${timeFrame}`);
        }
        return formatExpression;
    },
};
const warehouseConfigs = {
    [dbt_1.SupportedDbtAdapter.BIGQUERY]: bigqueryConfig,
    [dbt_1.SupportedDbtAdapter.SNOWFLAKE]: snowflakeConfig,
    [dbt_1.SupportedDbtAdapter.REDSHIFT]: postgresConfig,
    [dbt_1.SupportedDbtAdapter.POSTGRES]: postgresConfig,
    [dbt_1.SupportedDbtAdapter.DUCKDB]: postgresConfig,
    [dbt_1.SupportedDbtAdapter.DATABRICKS]: databricksConfig,
    [dbt_1.SupportedDbtAdapter.SPARK]: databricksConfig, // Spark uses same SQL dialect as Databricks
    [dbt_1.SupportedDbtAdapter.TRINO]: trinoConfig,
    [dbt_1.SupportedDbtAdapter.ATHENA]: trinoConfig, // Athena uses Trino SQL dialect
    [dbt_1.SupportedDbtAdapter.CLICKHOUSE]: clickhouseConfig,
};
/**
 * Generates DATE_TRUNC SQL. When a timezone is provided, the truncation is
 * performed in the project TZ and the result is converted back to a proper
 * UTC instant so downstream consumers apply .tz(project_tz) uniformly.
 */
// Adapters that keep the wider target == source skip: Databricks/Spark
// timestamps are instants (their wrap double-shifts them at equal zones) and
// Trino/Athena sessions do not rebase naive columns, so the wrap buys nothing
// there. Naive-column handling for these is tracked separately.
const equalZonesSkipAdapters = new Set([
    dbt_1.SupportedDbtAdapter.DATABRICKS,
    dbt_1.SupportedDbtAdapter.SPARK,
    dbt_1.SupportedDbtAdapter.TRINO,
    dbt_1.SupportedDbtAdapter.ATHENA,
]);
// Source defaults to UTC when unset (matches `getColumnTimezone`). On
// session-property warehouses the wrap is a genuine no-op only when both
// sides are UTC: for a naive column, target == source still requires the
// wrap's inner `castToInstant` rebase — skipping it misreads the wall clock
// as UTC downstream. The all-UTC skip avoids the cast that defeats partition
// pruning on warehouses like BigQuery. Every wrap site (dim trunc, extract,
// format, filter literal) shares this predicate so symmetry is enforced
// centrally.
const isTimezoneRoundTripNoOp = (adapterType, timezone, sourceTimezone) => {
    const source = sourceTimezone ?? 'UTC';
    if (equalZonesSkipAdapters.has(adapterType))
        return timezone === source;
    return timezone === 'UTC' && source === 'UTC';
};
exports.isTimezoneRoundTripNoOp = isTimezoneRoundTripNoOp;
// Returns the resolved (timezone, sourceTimezone) when the dim needs a tz
// wrap, else null. Wrap conditions: TIMESTAMP-typed dim AND a target timezone
// AND target != source.
const resolveTimezoneWrap = (adapterType, type, timezone, sourceTimezone) => {
    if (type !== field_1.DimensionType.TIMESTAMP || !timezone)
        return null;
    if ((0, exports.isTimezoneRoundTripNoOp)(adapterType, timezone, sourceTimezone))
        return null;
    return { timezone, sourceTimezone };
};
const getSqlForTruncatedDate = (adapterType, timeFrame, originalSql, type, startOfWeek, timezone, sourceTimezone, castDayOrCoarserToDate = false) => {
    const wrap = resolveTimezoneWrap(adapterType, type, timezone, sourceTimezone);
    // GLITCH-452: day-or-coarser grains emit a real DATE so the warehouse type
    // matches the metadata; sub-day grains stay TIMESTAMP.
    const castToDate = castDayOrCoarserToDate && !(0, exports.isSubDayTimeFrame)(timeFrame);
    if (!wrap) {
        const bare = warehouseConfigs[adapterType].getSqlForTruncatedDate(timeFrame, originalSql, type, startOfWeek);
        return castToDate ? `CAST(${bare} AS DATE)` : bare;
    }
    const { toProjectTz, toUTC, castAsDate } = exports.dateTruncTimezoneConversions[adapterType];
    const input = toProjectTz(originalSql, wrap.timezone, wrap.sourceTimezone);
    const truncated = warehouseConfigs[adapterType].getSqlForTruncatedDate(timeFrame, input, type, startOfWeek, wrap.timezone);
    // Day-or-coarser grains return a real DATE (drop the toUTC round-trip).
    // Every adapter's wrapped path now truncates to a naive wall-clock value
    // (BigQuery shifts to DATETIME in toProjectTz), so the per-adapter castAsDate
    // — CAST(... AS DATE) everywhere — reads the right calendar date in the
    // project tz. See `castAsDate` in dateTruncTimezoneConversions.
    if (!castToDate) {
        return toUTC(truncated, wrap.timezone);
    }
    return castAsDate(truncated, wrap.timezone);
};
exports.getSqlForTruncatedDate = getSqlForTruncatedDate;
// DATE base dimensions short-circuit: no time component to shift.
const getSqlForDatePart = (adapterType, timeFrame, originalSql, type, startOfWeek, timezone, sourceTimezone) => {
    const wrap = resolveTimezoneWrap(adapterType, type, timezone, sourceTimezone);
    const wrappedSql = wrap
        ? exports.dateExtractsTimezoneConversions[adapterType].toExtractInputTz(originalSql, wrap.timezone, wrap.sourceTimezone)
        : originalSql;
    return warehouseConfigs[adapterType].getSqlForDatePart(timeFrame, wrappedSql, type, startOfWeek);
};
exports.getSqlForDatePart = getSqlForDatePart;
// Unlike getSqlForDatePart, the wrap is applied per-adapter rather than
// centrally — BigQuery's name path uses native FORMAT_TIMESTAMP(fmt, ts, tz)
// and can't accept a pre-wrapped `... AT TIME ZONE` input.
const getSqlForDatePartName = (adapterType, timeFrame, originalSql, type, _startOfWeek, timezone, sourceTimezone) => {
    const wrap = resolveTimezoneWrap(adapterType, type, timezone, sourceTimezone);
    if (!wrap) {
        return warehouseConfigs[adapterType].getSqlForDatePartName(timeFrame, originalSql, type);
    }
    return warehouseConfigs[adapterType].getSqlForDatePartName(timeFrame, originalSql, type, wrap.timezone, wrap.sourceTimezone);
};
exports.getSqlForDatePartName = getSqlForDatePartName;
exports.timeFrameConfigs = {
    RAW: {
        getLabel: () => 'Raw',
        getDimensionType: (fallback) => fallback,
        getSql: (_adapterType, _timeInterval, originalSql) => originalSql,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
    MILLISECOND: {
        getLabel: () => 'Millisecond',
        getDimensionType: () => field_1.DimensionType.TIMESTAMP,
        getSql: exports.getSqlForTruncatedDate,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
    SECOND: {
        getLabel: () => 'Second',
        getDimensionType: () => field_1.DimensionType.TIMESTAMP,
        getSql: exports.getSqlForTruncatedDate,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
    MINUTE: {
        getLabel: () => 'Minute',
        getDimensionType: () => field_1.DimensionType.TIMESTAMP,
        getSql: exports.getSqlForTruncatedDate,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
    HOUR: {
        getLabel: () => 'Hour',
        getDimensionType: () => field_1.DimensionType.TIMESTAMP,
        getSql: exports.getSqlForTruncatedDate,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
    DAY: {
        getLabel: () => 'Day',
        getDimensionType: () => field_1.DimensionType.DATE,
        getSql: exports.getSqlForTruncatedDate,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => ({
            year: '{bold|{yyyy}}',
            month: '{bold|{MMM}}',
            day: '{d}',
            hour: '',
        }),
    },
    WEEK: {
        getLabel: () => 'Week',
        getDimensionType: () => field_1.DimensionType.DATE,
        getSql: exports.getSqlForTruncatedDate,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
    MONTH: {
        getLabel: () => 'Month',
        getDimensionType: () => field_1.DimensionType.DATE,
        getSql: exports.getSqlForTruncatedDate,
        getAxisMinInterval: () => 2629800000,
        getAxisLabelFormatter: () => null,
    },
    QUARTER: {
        getLabel: () => 'Quarter',
        getDimensionType: () => field_1.DimensionType.DATE,
        getSql: exports.getSqlForTruncatedDate,
        getAxisMinInterval: () => 7889400000,
        getAxisLabelFormatter: () => null,
    },
    YEAR: {
        getLabel: () => 'Year',
        getDimensionType: () => field_1.DimensionType.DATE,
        getSql: exports.getSqlForTruncatedDate,
        getAxisMinInterval: () => 31557600000,
        getAxisLabelFormatter: () => null,
    },
    WEEK_NUM: {
        getLabel: () => 'Week (number)',
        getDimensionType: () => field_1.DimensionType.NUMBER,
        getSql: exports.getSqlForDatePart,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
    MONTH_NUM: {
        getLabel: () => 'Month (number)',
        getDimensionType: () => field_1.DimensionType.NUMBER,
        getSql: exports.getSqlForDatePart,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
    DAY_OF_WEEK_INDEX: {
        getLabel: () => 'Day of the week (index)',
        getDimensionType: () => field_1.DimensionType.NUMBER,
        getSql: exports.getSqlForDatePart,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
    DAY_OF_MONTH_NUM: {
        getLabel: () => 'Day of the month (number)',
        getDimensionType: () => field_1.DimensionType.NUMBER,
        getSql: exports.getSqlForDatePart,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
    DAY_OF_YEAR_NUM: {
        getLabel: () => 'Day of the year (number)',
        getDimensionType: () => field_1.DimensionType.NUMBER,
        getSql: exports.getSqlForDatePart,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
    QUARTER_NUM: {
        getLabel: () => 'Quarter (number)',
        getDimensionType: () => field_1.DimensionType.NUMBER,
        getSql: exports.getSqlForDatePart,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
    YEAR_NUM: {
        getLabel: () => 'Year (number)',
        getDimensionType: () => field_1.DimensionType.NUMBER,
        getSql: exports.getSqlForDatePart,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
    DAY_OF_WEEK_NAME: {
        getLabel: () => 'Day of the week (name)',
        getDimensionType: () => field_1.DimensionType.STRING,
        getSql: exports.getSqlForDatePartName,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
    MONTH_NAME: {
        getLabel: () => 'Month (name)',
        getDimensionType: () => field_1.DimensionType.STRING,
        getSql: exports.getSqlForDatePartName,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
    QUARTER_NAME: {
        getLabel: () => 'Quarter (name)',
        getDimensionType: () => field_1.DimensionType.STRING,
        getSql: exports.getSqlForDatePartName,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
    HOUR_OF_DAY_NUM: {
        getLabel: () => 'Hour of day (number)',
        getDimensionType: () => field_1.DimensionType.NUMBER,
        getSql: exports.getSqlForDatePart,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
    MINUTE_OF_HOUR_NUM: {
        getLabel: () => 'Minute of hour (number)',
        getDimensionType: () => field_1.DimensionType.NUMBER,
        getSql: exports.getSqlForDatePart,
        getAxisMinInterval: () => null,
        getAxisLabelFormatter: () => null,
    },
};
const getDefaultTimeFrames = (type) => type === field_1.DimensionType.TIMESTAMP
    ? [
        timeFrames_1.TimeFrames.RAW,
        timeFrames_1.TimeFrames.DAY,
        timeFrames_1.TimeFrames.WEEK,
        timeFrames_1.TimeFrames.MONTH,
        timeFrames_1.TimeFrames.QUARTER,
        timeFrames_1.TimeFrames.YEAR,
    ]
    : [
        timeFrames_1.TimeFrames.DAY,
        timeFrames_1.TimeFrames.WEEK,
        timeFrames_1.TimeFrames.MONTH,
        timeFrames_1.TimeFrames.QUARTER,
        timeFrames_1.TimeFrames.YEAR,
    ];
exports.getDefaultTimeFrames = getDefaultTimeFrames;
/**
 * Built-in default time frames for a dimension type, with project-level
 * `additional_time_intervals` appended (de-duplicated, built-ins first).
 */
const getTimeFramesWithProjectDefaults = (type, additionalTimeIntervals) => {
    const additions = type === field_1.DimensionType.TIMESTAMP
        ? (additionalTimeIntervals?.timestamp ?? [])
        : (additionalTimeIntervals?.date ?? []);
    const seen = new Set();
    return [...(0, exports.getDefaultTimeFrames)(type), ...additions].filter((value) => {
        if (seen.has(value))
            return false;
        seen.add(value);
        return true;
    });
};
exports.getTimeFramesWithProjectDefaults = getTimeFramesWithProjectDefaults;
/** Time frames that use DATE_TRUNC (not EXTRACT/DATE_PART). */
exports.truncatableTimeFrames = new Set([
    timeFrames_1.TimeFrames.MILLISECOND,
    timeFrames_1.TimeFrames.SECOND,
    timeFrames_1.TimeFrames.MINUTE,
    timeFrames_1.TimeFrames.HOUR,
    timeFrames_1.TimeFrames.DAY,
    timeFrames_1.TimeFrames.WEEK,
    timeFrames_1.TimeFrames.MONTH,
    timeFrames_1.TimeFrames.QUARTER,
    timeFrames_1.TimeFrames.YEAR,
]);
/** Time frames that use EXTRACT/DATE_PART or format/name functions, not DATE_TRUNC. */
exports.extractableTimeFrames = new Set([
    timeFrames_1.TimeFrames.DAY_OF_WEEK_INDEX,
    timeFrames_1.TimeFrames.DAY_OF_MONTH_NUM,
    timeFrames_1.TimeFrames.DAY_OF_YEAR_NUM,
    timeFrames_1.TimeFrames.WEEK_NUM,
    timeFrames_1.TimeFrames.MONTH_NUM,
    timeFrames_1.TimeFrames.QUARTER_NUM,
    timeFrames_1.TimeFrames.YEAR_NUM,
    timeFrames_1.TimeFrames.HOUR_OF_DAY_NUM,
    timeFrames_1.TimeFrames.MINUTE_OF_HOUR_NUM,
    timeFrames_1.TimeFrames.DAY_OF_WEEK_NAME,
    timeFrames_1.TimeFrames.MONTH_NAME,
    timeFrames_1.TimeFrames.QUARTER_NAME,
]);
const isTimeInterval = (value) => Object.keys(exports.timeFrameConfigs).includes(value);
exports.isTimeInterval = isTimeInterval;
const validateTimeFrames = (values) => values.reduce((acc, value) => {
    const uppercaseValue = value.toUpperCase();
    return (0, exports.isTimeInterval)(uppercaseValue) ? [...acc, uppercaseValue] : acc;
}, []);
exports.validateTimeFrames = validateTimeFrames;
exports.timeFrameOrder = [
    undefined,
    timeFrames_1.TimeFrames.RAW,
    timeFrames_1.TimeFrames.MILLISECOND,
    timeFrames_1.TimeFrames.SECOND,
    timeFrames_1.TimeFrames.MINUTE,
    timeFrames_1.TimeFrames.HOUR,
    timeFrames_1.TimeFrames.DAY,
    timeFrames_1.TimeFrames.DAY_OF_WEEK_INDEX,
    timeFrames_1.TimeFrames.DAY_OF_WEEK_NAME,
    timeFrames_1.TimeFrames.DAY_OF_MONTH_NUM,
    timeFrames_1.TimeFrames.DAY_OF_YEAR_NUM,
    timeFrames_1.TimeFrames.WEEK,
    timeFrames_1.TimeFrames.WEEK_NUM,
    timeFrames_1.TimeFrames.MONTH,
    timeFrames_1.TimeFrames.MONTH_NUM,
    timeFrames_1.TimeFrames.MONTH_NAME,
    timeFrames_1.TimeFrames.QUARTER,
    timeFrames_1.TimeFrames.QUARTER_NUM,
    timeFrames_1.TimeFrames.QUARTER_NAME,
    timeFrames_1.TimeFrames.YEAR,
    timeFrames_1.TimeFrames.YEAR_NUM,
    timeFrames_1.TimeFrames.HOUR_OF_DAY_NUM,
    timeFrames_1.TimeFrames.MINUTE_OF_HOUR_NUM,
];
const sortTimeFrames = (a, b) => exports.timeFrameOrder.indexOf(a) - exports.timeFrameOrder.indexOf(b);
exports.sortTimeFrames = sortTimeFrames;
exports.SUB_DAY_GRANULARITIES = new Set([
    timeFrames_1.DateGranularity.SECOND,
    timeFrames_1.DateGranularity.MINUTE,
    timeFrames_1.DateGranularity.HOUR,
]);
const isSubDayGranularity = (g) => exports.SUB_DAY_GRANULARITIES.has(g);
exports.isSubDayGranularity = isSubDayGranularity;
const getDateDimension = (dimensionId) => {
    const timeFrames = Object.values(timeFrames_1.DateGranularity).map((tf) => tf.toLowerCase());
    const isDate = timeFrames.some((timeFrame) => dimensionId.endsWith(timeFrame));
    if (isDate) {
        const regex = new RegExp(`_(${timeFrames.join('|')})$`);
        const baseDimensionId = dimensionId.replace(regex, '');
        const timeString = dimensionId.replace(`${baseDimensionId}_`, '');
        const [newTimeFrame] = (0, exports.validateTimeFrames)([timeString]);
        if (baseDimensionId && newTimeFrame)
            return {
                baseDimensionId,
                newTimeFrame,
            };
    }
    return {};
};
exports.getDateDimension = getDateDimension;
//# sourceMappingURL=timeFrames.js.map