From 4b71e9aa645117a944af409882bda33f64225bf9 Mon Sep 17 00:00:00 2001 From: Nemanja Glumac <31325167+nemanjaglumac@users.noreply.github.com> Date: Tue, 23 Apr 2024 10:31:45 +0200 Subject: [PATCH] Use the proper case for a "year" token format (#41715) * Use proper case for year date format Fixes #40493 * Add regression test for specific filters in different locales * Actually use the locale --- frontend/src/metabase-lib/filter.ts | 2 +- frontend/src/metabase-lib/filter.unit.spec.ts | 343 +++++++++--------- 2 files changed, 176 insertions(+), 169 deletions(-) diff --git a/frontend/src/metabase-lib/filter.ts b/frontend/src/metabase-lib/filter.ts index e46ba9547f5..4f9b20ed2da 100644 --- a/frontend/src/metabase-lib/filter.ts +++ b/frontend/src/metabase-lib/filter.ts @@ -679,7 +679,7 @@ function isExcludeDateBucket( return buckets.includes(bucketName); } -const DATE_FORMAT = "yyyy-MM-DD"; +const DATE_FORMAT = "YYYY-MM-DD"; const TIME_FORMAT = "HH:mm:ss"; const TIME_FORMATS = ["HH:mm:ss.SSS[Z]", "HH:mm:ss.SSS", "HH:mm:ss", "HH:mm"]; const TIME_FORMAT_MS = "HH:mm:ss.SSS"; diff --git a/frontend/src/metabase-lib/filter.unit.spec.ts b/frontend/src/metabase-lib/filter.unit.spec.ts index 4ab60162d3c..86ada8a14ec 100644 --- a/frontend/src/metabase-lib/filter.unit.spec.ts +++ b/frontend/src/metabase-lib/filter.unit.spec.ts @@ -1,3 +1,5 @@ +import moment from "moment-timezone"; // eslint-disable-line no-restricted-imports -- deprecated usage + import { createMockMetadata } from "__support__/metadata"; import * as Lib from "metabase-lib"; import { @@ -767,240 +769,245 @@ describe("filter", () => { }); }); - describe("specific date filters", () => { - const tableName = "PRODUCTS"; - const columnName = "CREATED_AT"; - const column = findColumn(query, tableName, columnName); + describe.each(["en", "ja", "ar", "th", "ko", "vi", "zh"])( + "specific date filters for locale %s", + locale => { + const tableName = "PRODUCTS"; + const columnName = "CREATED_AT"; + const column = findColumn(query, tableName, columnName); + + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date(2020, 0, 1)); + moment.locale(locale); + }); - beforeEach(() => { - jest.useFakeTimers(); - jest.setSystemTime(new Date(2020, 0, 1)); - }); + it.each<Lib.SpecificDateFilterOperatorName>(["=", ">", "<"])( + 'should be able to create and destructure a specific date filter with "%s" operator and 1 value', + operator => { + const values = [new Date(2018, 2, 10)]; + const { filterParts, columnInfo, bucketInfo } = addSpecificDateFilter( + query, + Lib.specificDateFilterClause(query, 0, { + operator, + column, + values, + }), + ); - it.each<Lib.SpecificDateFilterOperatorName>(["=", ">", "<"])( - 'should be able to create and destructure a specific date filter with "%s" operator and 1 value', - operator => { - const values = [new Date(2018, 2, 10)]; + expect(filterParts).toMatchObject({ + operator, + column: expect.anything(), + values, + }); + expect(columnInfo?.name).toBe(columnName); + expect(bucketInfo).toBe(null); + }, + ); + + it('should be able to create and destructure a specific date filter with "between" operator and 2 values', () => { + moment.locale("ja"); + const values = [new Date(2018, 2, 10), new Date(2019, 10, 20)]; const { filterParts, columnInfo, bucketInfo } = addSpecificDateFilter( query, Lib.specificDateFilterClause(query, 0, { - operator, + operator: "between", column, values, }), ); expect(filterParts).toMatchObject({ - operator, + operator: "between", column: expect.anything(), values, }); expect(columnInfo?.name).toBe(columnName); expect(bucketInfo).toBe(null); - }, - ); + }); - it('should be able to create and destructure a specific date filter with "between" operator and 2 values', () => { - const values = [new Date(2018, 2, 10), new Date(2019, 10, 20)]; - const { filterParts, columnInfo, bucketInfo } = addSpecificDateFilter( - query, - Lib.specificDateFilterClause(query, 0, { - operator: "between", - column, - values, - }), + it.each<Lib.SpecificDateFilterOperatorName>(["=", ">", "<"])( + 'should remove an existing temporal bucket with "%s" operator and 1 value', + operator => { + const values = [new Date(2018, 2, 10)]; + const { filterParts, columnInfo, bucketInfo } = addSpecificDateFilter( + query, + Lib.specificDateFilterClause(query, 0, { + operator, + column: Lib.withTemporalBucket( + column, + findTemporalBucket(query, column, "Day"), + ), + values, + }), + ); + + expect(filterParts).toMatchObject({ + operator, + column: expect.anything(), + values, + }); + expect(columnInfo?.name).toBe(columnName); + expect(bucketInfo).toBe(null); + }, ); - expect(filterParts).toMatchObject({ - operator: "between", - column: expect.anything(), - values, - }); - expect(columnInfo?.name).toBe(columnName); - expect(bucketInfo).toBe(null); - }); - - it.each<Lib.SpecificDateFilterOperatorName>(["=", ">", "<"])( - 'should remove an existing temporal bucket with "%s" operator and 1 value', - operator => { - const values = [new Date(2018, 2, 10)]; + it('should remove an existing temporal bucket with "between" operator and 2 values', () => { + const values = [new Date(2018, 2, 10), new Date(2019, 10, 20)]; const { filterParts, columnInfo, bucketInfo } = addSpecificDateFilter( query, Lib.specificDateFilterClause(query, 0, { - operator, + operator: "between", column: Lib.withTemporalBucket( column, - findTemporalBucket(query, column, "Day"), + findTemporalBucket(query, column, "Hour"), ), values, }), ); expect(filterParts).toMatchObject({ - operator, + operator: "between", column: expect.anything(), values, }); expect(columnInfo?.name).toBe(columnName); expect(bucketInfo).toBe(null); - }, - ); + }); - it('should remove an existing temporal bucket with "between" operator and 2 values', () => { - const values = [new Date(2018, 2, 10), new Date(2019, 10, 20)]; - const { filterParts, columnInfo, bucketInfo } = addSpecificDateFilter( - query, - Lib.specificDateFilterClause(query, 0, { - operator: "between", - column: Lib.withTemporalBucket( - column, - findTemporalBucket(query, column, "Hour"), - ), - values, - }), - ); + it.each<Lib.SpecificDateFilterOperatorName>(["=", ">", "<"])( + 'should set "minute" temporal bucket with "%s" operator and 1 value if there are time parts', + operator => { + const values = [new Date(2018, 2, 10, 30)]; + const { filterParts, columnInfo, bucketInfo } = addSpecificDateFilter( + query, + Lib.specificDateFilterClause(query, 0, { + operator, + column, + values, + }), + ); - expect(filterParts).toMatchObject({ - operator: "between", - column: expect.anything(), - values, - }); - expect(columnInfo?.name).toBe(columnName); - expect(bucketInfo).toBe(null); - }); + expect(filterParts).toMatchObject({ + operator, + column: expect.anything(), + values, + }); + expect(columnInfo?.name).toBe(columnName); + expect(bucketInfo?.shortName).toBe("minute"); + }, + ); - it.each<Lib.SpecificDateFilterOperatorName>(["=", ">", "<"])( - 'should set "minute" temporal bucket with "%s" operator and 1 value if there are time parts', - operator => { - const values = [new Date(2018, 2, 10, 30)]; + it('should set "minute" temporal bucket with "between" operator and 1 value if there are time parts', () => { + const values = [new Date(2018, 2, 10), new Date(2019, 10, 20, 15)]; const { filterParts, columnInfo, bucketInfo } = addSpecificDateFilter( query, Lib.specificDateFilterClause(query, 0, { - operator, + operator: "between", column, values, }), ); expect(filterParts).toMatchObject({ - operator, + operator: "between", column: expect.anything(), values, }); expect(columnInfo?.name).toBe(columnName); expect(bucketInfo?.shortName).toBe("minute"); - }, - ); - - it('should set "minute" temporal bucket with "between" operator and 1 value if there are time parts', () => { - const values = [new Date(2018, 2, 10), new Date(2019, 10, 20, 15)]; - const { filterParts, columnInfo, bucketInfo } = addSpecificDateFilter( - query, - Lib.specificDateFilterClause(query, 0, { - operator: "between", - column, - values, - }), - ); - - expect(filterParts).toMatchObject({ - operator: "between", - column: expect.anything(), - values, }); - expect(columnInfo?.name).toBe(columnName); - expect(bucketInfo?.shortName).toBe("minute"); - }); - it.each([ - ["yyyy-MM-DDTHH:mm:ssZ", "2020-01-05T10:20:00+01:00"], - ["yyyy-MM-DDTHH:mm:ss", "2020-01-05T10:20:00"], - ["yyyy-MM-DD", "2020-01-05"], - ])("should support %s date format", (format, arg) => { - const { filterParts } = addSpecificDateFilter( - query, - Lib.expressionClause("=", [column, arg]), - ); - expect(filterParts).toMatchObject({ - operator: "=", - column: expect.anything(), - values: [expect.any(Date)], + it.each([ + ["yyyy-MM-DDTHH:mm:ssZ", "2020-01-05T10:20:00+01:00"], + ["yyyy-MM-DDTHH:mm:ss", "2020-01-05T10:20:00"], + ["yyyy-MM-DD", "2020-01-05"], + ])("should support %s date format", (format, arg) => { + const { filterParts } = addSpecificDateFilter( + query, + Lib.expressionClause("=", [column, arg]), + ); + expect(filterParts).toMatchObject({ + operator: "=", + column: expect.anything(), + values: [expect.any(Date)], + }); + + const value = filterParts?.values[0]; + expect(value?.getFullYear()).toBe(2020); + expect(value?.getMonth()).toBe(0); + expect(value?.getDate()).toBe(5); }); - const value = filterParts?.values[0]; - expect(value?.getFullYear()).toBe(2020); - expect(value?.getMonth()).toBe(0); - expect(value?.getDate()).toBe(5); - }); + it.each([ + ["2020-01-05T00:00:00.000", new Date(2020, 0, 5, 0, 0, 0, 0)], + ["2020-01-05T00:00:00.001", new Date(2020, 0, 5, 0, 0, 0, 1)], + ["2020-01-05T00:00:00", new Date(2020, 0, 5, 0, 0, 0)], + ["2020-01-05T00:00:01", new Date(2020, 0, 5, 0, 0, 1)], + ["2020-01-05T00:01:00", new Date(2020, 0, 5, 0, 1, 0)], + ["2020-01-05T01:00:00", new Date(2020, 0, 5, 1, 0, 0)], + ["2020-01-05T10:20:30", new Date(2020, 0, 5, 10, 20, 30)], + ["2020-01-05T10:20:30+04:00", new Date(2020, 0, 5, 10, 20, 30)], + ])("should support %s datetime format", (arg, date) => { + const { filterParts } = addSpecificDateFilter( + query, + Lib.expressionClause("=", [column, arg]), + ); + expect(filterParts).toMatchObject({ + operator: "=", + column: expect.anything(), + values: [expect.any(Date)], + }); - it.each([ - ["2020-01-05T00:00:00.000", new Date(2020, 0, 5, 0, 0, 0, 0)], - ["2020-01-05T00:00:00.001", new Date(2020, 0, 5, 0, 0, 0, 1)], - ["2020-01-05T00:00:00", new Date(2020, 0, 5, 0, 0, 0)], - ["2020-01-05T00:00:01", new Date(2020, 0, 5, 0, 0, 1)], - ["2020-01-05T00:01:00", new Date(2020, 0, 5, 0, 1, 0)], - ["2020-01-05T01:00:00", new Date(2020, 0, 5, 1, 0, 0)], - ["2020-01-05T10:20:30", new Date(2020, 0, 5, 10, 20, 30)], - ["2020-01-05T10:20:30+04:00", new Date(2020, 0, 5, 10, 20, 30)], - ])("should support %s datetime format", (arg, date) => { - const { filterParts } = addSpecificDateFilter( - query, - Lib.expressionClause("=", [column, arg]), - ); - expect(filterParts).toMatchObject({ - operator: "=", - column: expect.anything(), - values: [expect.any(Date)], + const value = filterParts?.values[0]; + expect(value?.getFullYear()).toBe(date.getFullYear()); + expect(value?.getMonth()).toBe(date.getMonth()); + expect(value?.getDate()).toBe(date.getDate()); + expect(value?.getHours()).toBe(date.getHours()); + expect(value?.getMinutes()).toBe(date.getMinutes()); }); - const value = filterParts?.values[0]; - expect(value?.getFullYear()).toBe(date.getFullYear()); - expect(value?.getMonth()).toBe(date.getMonth()); - expect(value?.getDate()).toBe(date.getDate()); - expect(value?.getHours()).toBe(date.getHours()); - expect(value?.getMinutes()).toBe(date.getMinutes()); - }); - - it("should ignore expressions with not supported operators", () => { - const { filterParts } = addSpecificDateFilter( - query, - Lib.expressionClause("!=", [column, "2020-01-01"]), - ); + it("should ignore expressions with not supported operators", () => { + const { filterParts } = addSpecificDateFilter( + query, + Lib.expressionClause("!=", [column, "2020-01-01"]), + ); - expect(filterParts).toBeNull(); - }); + expect(filterParts).toBeNull(); + }); - it("should ignore expressions without first column", () => { - const { filterParts } = addSpecificDateFilter( - query, - Lib.expressionClause("=", ["2020-01-01", column]), - ); + it("should ignore expressions without first column", () => { + const { filterParts } = addSpecificDateFilter( + query, + Lib.expressionClause("=", ["2020-01-01", column]), + ); - expect(filterParts).toBeNull(); - }); + expect(filterParts).toBeNull(); + }); - it("should ignore expressions with non-time arguments", () => { - const { filterParts } = addSpecificDateFilter( - query, - Lib.expressionClause("=", [column, column]), - ); + it("should ignore expressions with non-time arguments", () => { + const { filterParts } = addSpecificDateFilter( + query, + Lib.expressionClause("=", [column, column]), + ); - expect(filterParts).toBeNull(); - }); + expect(filterParts).toBeNull(); + }); - it("should ignore expressions with incorrect column type", () => { - const { filterParts } = addSpecificDateFilter( - query, - Lib.specificDateFilterClause(query, 0, { - operator: "=", - column: findColumn(query, tableName, "PRICE"), - values: [new Date(2020, 1, 1)], - }), - ); + it("should ignore expressions with incorrect column type", () => { + const { filterParts } = addSpecificDateFilter( + query, + Lib.specificDateFilterClause(query, 0, { + operator: "=", + column: findColumn(query, tableName, "PRICE"), + values: [new Date(2020, 1, 1)], + }), + ); - expect(filterParts).toBeNull(); - }); - }); + expect(filterParts).toBeNull(); + }); + }, + ); describe("relative date filters", () => { const tableName = "PRODUCTS"; -- GitLab