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