From c85ca2355cb0b6fcef73f3e7497f7c31fa82c87c Mon Sep 17 00:00:00 2001
From: Vladimir Potekhin <46284632+vladimirpotekhin@users.noreply.github.com>
Date: Thu, 16 Mar 2023 18:44:17 +0300
Subject: [PATCH] feat(kit): use 1 as min segment value in `Date`-related masks
 (#197)

---
 .../kit/date-range/date-range-basic.cy.ts     | 12 +++++-----
 .../tests/kit/date-time/date-time-basic.cy.ts |  6 +++--
 .../cypress/tests/kit/date/date-basic.cy.ts   | 22 +++++++++++++++--
 .../lib/masks/date-range/date-range-mask.ts   |  1 +
 .../min-max-date-time-postprocessor.ts        | 15 +++++++++---
 projects/kit/src/lib/masks/date/date-mask.ts  |  7 +++++-
 .../processors/min-max-date-postprocessor.ts  | 15 ++++++++++--
 .../utils/date/date-segment-value-length.ts   |  6 ++---
 .../utils/date/raise-segment-value-to-min.ts  | 24 +++++++++++++++++++
 .../get-date-segment-value-length.spec.ts     | 15 ++++++++++++
 .../lib/utils/date/validate-date-string.ts    |  7 +++++-
 11 files changed, 110 insertions(+), 20 deletions(-)
 create mode 100644 projects/kit/src/lib/utils/date/raise-segment-value-to-min.ts
 create mode 100644 projects/kit/src/lib/utils/date/tests/get-date-segment-value-length.spec.ts

diff --git a/projects/demo-integrations/cypress/tests/kit/date-range/date-range-basic.cy.ts b/projects/demo-integrations/cypress/tests/kit/date-range/date-range-basic.cy.ts
index 8188de8f0..6126259b7 100644
--- a/projects/demo-integrations/cypress/tests/kit/date-range/date-range-basic.cy.ts
+++ b/projects/demo-integrations/cypress/tests/kit/date-range/date-range-basic.cy.ts
@@ -166,7 +166,7 @@ describe('DateRange | Basic', () => {
                 .should('have.prop', 'selectionEnd', '25.02.18'.length);
         });
 
-        it('13.06.1736 - 14.09|.1821 => Backspace => 13.06.1736 - 14.0|0.1821 => Type "3" => 13.06.1736 - 14.03|.1821', () => {
+        it('13.06.1736 - 14.09|.1821 => Backspace => 13.06.1736 - 14.0|1.1821 => Type "3" => 13.06.1736 - 14.03|.1821', () => {
             cy.get('@input')
                 .type('13.06.1736-14.09.1821')
                 .should('have.value', '13.06.1736 – 14.09.1821')
@@ -174,7 +174,7 @@ describe('DateRange | Basic', () => {
                 .should('have.prop', 'selectionStart', '13.06.1736 - 14.09'.length)
                 .should('have.prop', 'selectionEnd', '13.06.1736 - 14.09'.length)
                 .type('{backspace}')
-                .should('have.value', '13.06.1736 – 14.00.1821')
+                .should('have.value', '13.06.1736 – 14.01.1821')
                 .should('have.prop', 'selectionStart', '13.06.1736 - 14.0'.length)
                 .should('have.prop', 'selectionEnd', '13.06.1736 - 14.0'.length)
                 .type('3')
@@ -300,7 +300,7 @@ describe('DateRange | Basic', () => {
 
     describe('Text selection', () => {
         describe('Select range and press Backspace / Delete', () => {
-            it('10.|12|.2005 - 16.12.2007 => Backspace => 10.|00.2005 - 16.12.2007', () => {
+            it('10.|12|.2005 - 16.12.2007 => Backspace => 10.|01.2005 - 16.12.2007', () => {
                 cy.get('@input')
                     .type('10122005-16122007')
                     .should('have.value', '10.12.2005 – 16.12.2007')
@@ -312,12 +312,12 @@ describe('DateRange | Basic', () => {
                     ]);
 
                 cy.get('@input')
-                    .should('have.value', '10.00.2005 – 16.12.2007')
+                    .should('have.value', '10.01.2005 – 16.12.2007')
                     .should('have.prop', 'selectionStart', '10.'.length)
                     .should('have.prop', 'selectionEnd', '10.'.length);
             });
 
-            it('10.12.2005 - |16|.12.2007 => Backspace => 10.12.2005 - |00.12.2007', () => {
+            it('10.12.2005 - |16|.12.2007 => Backspace => 10.12.2005 - |01.12.2007', () => {
                 cy.get('@input')
                     .type('10122005-16122007')
                     .should('have.value', '10.12.2005 – 16.12.2007')
@@ -329,7 +329,7 @@ describe('DateRange | Basic', () => {
                     ]);
 
                 cy.get('@input')
-                    .should('have.value', '10.12.2005 – 00.12.2007')
+                    .should('have.value', '10.12.2005 – 01.12.2007')
                     .should('have.prop', 'selectionStart', '10.12.2005 - '.length)
                     .should('have.prop', 'selectionEnd', '10.12.2005 - '.length);
             });
diff --git a/projects/demo-integrations/cypress/tests/kit/date-time/date-time-basic.cy.ts b/projects/demo-integrations/cypress/tests/kit/date-time/date-time-basic.cy.ts
index 52a3b214e..3f2b8cd2d 100644
--- a/projects/demo-integrations/cypress/tests/kit/date-time/date-time-basic.cy.ts
+++ b/projects/demo-integrations/cypress/tests/kit/date-time/date-time-basic.cy.ts
@@ -24,6 +24,8 @@ describe('DateTime | Basic', () => {
             ['1811201623152', '18.11.2016, 23:15:2'],
             ['18112016231522', '18.11.2016, 23:15:22'],
             ['18112016231522123', '18.11.2016, 23:15:22.123'],
+            ['0', '0'],
+            ['00', '0'],
         ] as const;
 
         tests.forEach(([typedValue, maskedValue]) => {
@@ -219,7 +221,7 @@ describe('DateTime | Basic', () => {
 
     describe('Text selection', () => {
         describe('Select range and press Backspace / Delete', () => {
-            it('10.|12|.2005, 12:30 => Backspace => 10.|00.2005, 12:30', () => {
+            it('10.|12|.2005, 12:30 => Backspace => 10.|01.2005, 12:30', () => {
                 cy.get('@input')
                     .type('101220051230')
                     .should('have.value', '10.12.2005, 12:30')
@@ -231,7 +233,7 @@ describe('DateTime | Basic', () => {
                     ]);
 
                 cy.get('@input')
-                    .should('have.value', '10.00.2005, 12:30')
+                    .should('have.value', '10.01.2005, 12:30')
                     .should('have.prop', 'selectionStart', '10.'.length)
                     .should('have.prop', 'selectionEnd', '10.'.length);
             });
diff --git a/projects/demo-integrations/cypress/tests/kit/date/date-basic.cy.ts b/projects/demo-integrations/cypress/tests/kit/date/date-basic.cy.ts
index df7cd1743..0102466ea 100644
--- a/projects/demo-integrations/cypress/tests/kit/date/date-basic.cy.ts
+++ b/projects/demo-integrations/cypress/tests/kit/date/date-basic.cy.ts
@@ -18,6 +18,8 @@ describe('Date', () => {
                 ['12', '12', '12'.length],
                 ['121', '12.1', '12.1'.length],
                 ['1211', '12.11', '12.11'.length],
+                ['0', '0', 1],
+                ['00', '0', '0'.length],
             ] as const;
 
             tests.forEach(([typedValue, maskedValue, caretIndex]) => {
@@ -176,6 +178,22 @@ describe('Date', () => {
                     .should('have.prop', 'selectionStart', '3'.length)
                     .should('have.prop', 'selectionEnd', '3'.length);
             });
+
+            it('02|.01.2008 => Backspace => 0|1.01.2008 => Type "5" => 05|.01.2008', () => {
+                cy.get('@input')
+                    .type('02012008')
+                    .type('{leftArrow}'.repeat('.01.2008'.length))
+                    .should('have.prop', 'selectionStart', '02'.length)
+                    .should('have.prop', 'selectionEnd', '02'.length)
+                    .type('{backspace}')
+                    .should('have.value', '01.01.2008')
+                    .should('have.prop', 'selectionStart', '0'.length)
+                    .should('have.prop', 'selectionEnd', '0'.length)
+                    .type('5')
+                    .should('have.value', '05.01.2008')
+                    .should('have.prop', 'selectionStart', '05.'.length)
+                    .should('have.prop', 'selectionEnd', '05.'.length);
+            });
         });
 
         describe('Fixed values', () => {
@@ -206,7 +224,7 @@ describe('Date', () => {
 
         describe('Text selection', () => {
             describe('Select range and press Backspace', () => {
-                it('10.|12|.2022 => Backspace => 10.|00.2022', () => {
+                it('10.|12|.2022 => Backspace => 10.|01.2022', () => {
                     cy.get('@input')
                         .type('10122022')
                         .type('{leftArrow}'.repeat('.2022'.length))
@@ -217,7 +235,7 @@ describe('Date', () => {
                         ]);
 
                     cy.get('@input')
-                        .should('have.value', '10.00.2022')
+                        .should('have.value', '10.01.2022')
                         .should('have.prop', 'selectionStart', '10.'.length)
                         .should('have.prop', 'selectionEnd', '10.'.length);
                 });
diff --git a/projects/kit/src/lib/masks/date-range/date-range-mask.ts b/projects/kit/src/lib/masks/date-range/date-range-mask.ts
index 70f46d015..47297d0d2 100644
--- a/projects/kit/src/lib/masks/date-range/date-range-mask.ts
+++ b/projects/kit/src/lib/masks/date-range/date-range-mask.ts
@@ -48,6 +48,7 @@ export function maskitoDateRangeOptionsGenerator({
                 max,
                 dateModeTemplate,
                 datesSeparator: DATE_RANGE_SEPARATOR,
+                dateSegmentSeparator: separator,
             }),
             createMinMaxRangeLengthPostprocessor({
                 dateModeTemplate,
diff --git a/projects/kit/src/lib/masks/date-time/postprocessors/min-max-date-time-postprocessor.ts b/projects/kit/src/lib/masks/date-time/postprocessors/min-max-date-time-postprocessor.ts
index b2ca28e05..6ed4a04c1 100644
--- a/projects/kit/src/lib/masks/date-time/postprocessors/min-max-date-time-postprocessor.ts
+++ b/projects/kit/src/lib/masks/date-time/postprocessors/min-max-date-time-postprocessor.ts
@@ -9,6 +9,7 @@ import {
     segmentsToDate,
     toDateString,
 } from '../../../utils';
+import {raiseSegmentValueToMin} from '../../../utils/date/raise-segment-value-to-min';
 import {parseTimeString} from '../../../utils/time';
 import {isDateTimeStringComplete, parseDateTimeString} from '../utils';
 
@@ -25,16 +26,24 @@ export function createMinMaxDateTimePostprocessor({
 }): NonNullable<MaskitoOptions['postprocessor']> {
     return ({value, selection}) => {
         const [dateString, timeString] = parseDateTimeString(value, dateModeTemplate);
+        const parsedDate = parseDateString(dateString, dateModeTemplate);
+        const parsedTime = parseTimeString(timeString);
 
         if (!isDateTimeStringComplete(value, dateModeTemplate, timeMode)) {
+            const fixedDate = raiseSegmentValueToMin(parsedDate, dateModeTemplate);
+            const fixedValue = toDateString(
+                {...fixedDate, ...parsedTime},
+                dateModeTemplate,
+                timeMode,
+            );
+            const tail = value.slice(fixedValue.length);
+
             return {
                 selection,
-                value: value,
+                value: fixedValue + tail,
             };
         }
 
-        const parsedDate = parseDateString(dateString, dateModeTemplate);
-        const parsedTime = parseTimeString(timeString);
         const date = segmentsToDate(parsedDate, parsedTime);
         const clampedDate = clamp(date, min, max);
 
diff --git a/projects/kit/src/lib/masks/date/date-mask.ts b/projects/kit/src/lib/masks/date/date-mask.ts
index adb0428f3..12ec88558 100644
--- a/projects/kit/src/lib/masks/date/date-mask.ts
+++ b/projects/kit/src/lib/masks/date/date-mask.ts
@@ -32,6 +32,11 @@ export function maskitoDateOptionsGenerator({
                 dateSegmentsSeparator: separator,
             }),
         ),
-        postprocessor: createMinMaxDatePostprocessor({min, max, dateModeTemplate}),
+        postprocessor: createMinMaxDatePostprocessor({
+            min,
+            max,
+            dateModeTemplate,
+            dateSegmentSeparator: separator,
+        }),
     };
 }
diff --git a/projects/kit/src/lib/processors/min-max-date-postprocessor.ts b/projects/kit/src/lib/processors/min-max-date-postprocessor.ts
index c7ded16b3..c66892d54 100644
--- a/projects/kit/src/lib/processors/min-max-date-postprocessor.ts
+++ b/projects/kit/src/lib/processors/min-max-date-postprocessor.ts
@@ -10,17 +10,20 @@ import {
     segmentsToDate,
     toDateString,
 } from '../utils';
+import {raiseSegmentValueToMin} from '../utils/date/raise-segment-value-to-min';
 
 export function createMinMaxDatePostprocessor({
     dateModeTemplate,
     min = DEFAULT_MIN_DATE,
     max = DEFAULT_MAX_DATE,
     datesSeparator = '',
+    dateSegmentSeparator = '.',
 }: {
     dateModeTemplate: string;
     min?: Date;
     max?: Date;
     datesSeparator?: string;
+    dateSegmentSeparator?: string;
 }): NonNullable<MaskitoOptions['postprocessor']> {
     return ({value, selection}) => {
         const endsWithDatesSeparator = datesSeparator && value.endsWith(datesSeparator);
@@ -31,12 +34,20 @@ export function createMinMaxDatePostprocessor({
         for (const dateString of dateStrings) {
             validatedValue += validatedValue ? datesSeparator : '';
 
+            const parsedDate = parseDateString(dateString, dateModeTemplate);
+
             if (!isDateStringComplete(dateString, dateModeTemplate)) {
-                validatedValue += dateString;
+                const fixedDate = raiseSegmentValueToMin(parsedDate, dateModeTemplate);
+
+                const fixedValue = toDateString(fixedDate, dateModeTemplate);
+                const tail = dateString.endsWith(dateSegmentSeparator)
+                    ? dateSegmentSeparator
+                    : '';
+
+                validatedValue += fixedValue + tail;
                 continue;
             }
 
-            const parsedDate = parseDateString(dateString, dateModeTemplate);
             const date = segmentsToDate(parsedDate);
             const clampedDate = clamp(date, min, max);
 
diff --git a/projects/kit/src/lib/utils/date/date-segment-value-length.ts b/projects/kit/src/lib/utils/date/date-segment-value-length.ts
index f69ab93f6..72222f141 100644
--- a/projects/kit/src/lib/utils/date/date-segment-value-length.ts
+++ b/projects/kit/src/lib/utils/date/date-segment-value-length.ts
@@ -3,7 +3,7 @@ import {MaskitoDateSegments} from '../../types';
 export const getDateSegmentValueLength: (
     dateString: string,
 ) => MaskitoDateSegments<number> = (dateString: string) => ({
-    day: dateString.match('/d/g')?.length || 2,
-    month: dateString.match('/m/g')?.length || 2,
-    year: dateString.match('/y/g')?.length || 4,
+    day: dateString.match(/d/g)?.length || 0,
+    month: dateString.match(/m/g)?.length || 0,
+    year: dateString.match(/y/g)?.length || 0,
 });
diff --git a/projects/kit/src/lib/utils/date/raise-segment-value-to-min.ts b/projects/kit/src/lib/utils/date/raise-segment-value-to-min.ts
new file mode 100644
index 000000000..7d7a66d63
--- /dev/null
+++ b/projects/kit/src/lib/utils/date/raise-segment-value-to-min.ts
@@ -0,0 +1,24 @@
+import {MaskitoDateSegments} from '../../types';
+import {getObjectFromEntries} from '../get-object-from-entries';
+import {getDateSegmentValueLength} from './date-segment-value-length';
+
+export function raiseSegmentValueToMin(
+    segments: Partial<MaskitoDateSegments>,
+    fullMode: string,
+): Partial<MaskitoDateSegments> {
+    const segmentsLength = getDateSegmentValueLength(fullMode);
+
+    return getObjectFromEntries(
+        Object.entries<string>(segments).map(([key, value]: [string, string]) => {
+            const segmentLength =
+                segmentsLength[key as keyof Partial<MaskitoDateSegments>];
+
+            return [
+                key,
+                value.length === segmentLength && value.match(/^0+$/)
+                    ? '1'.padStart(segmentLength, '0')
+                    : value,
+            ];
+        }),
+    );
+}
diff --git a/projects/kit/src/lib/utils/date/tests/get-date-segment-value-length.spec.ts b/projects/kit/src/lib/utils/date/tests/get-date-segment-value-length.spec.ts
new file mode 100644
index 000000000..e9f41da73
--- /dev/null
+++ b/projects/kit/src/lib/utils/date/tests/get-date-segment-value-length.spec.ts
@@ -0,0 +1,15 @@
+import {getDateSegmentValueLength} from '../date-segment-value-length';
+
+describe('getDateSegmentValueLength', () => {
+    it('short date', () => {
+        expect(getDateSegmentValueLength('mm.yy')).toEqual({day: 0, month: 2, year: 2});
+    });
+
+    it('full date', () => {
+        expect(getDateSegmentValueLength('dd.mm.yyyy')).toEqual({
+            day: 2,
+            month: 2,
+            year: 4,
+        });
+    });
+});
diff --git a/projects/kit/src/lib/utils/date/validate-date-string.ts b/projects/kit/src/lib/utils/date/validate-date-string.ts
index 4f82150a8..c39145dee 100644
--- a/projects/kit/src/lib/utils/date/validate-date-string.ts
+++ b/projects/kit/src/lib/utils/date/validate-date-string.ts
@@ -39,7 +39,7 @@ export function validateDateString({
             offset +
             validatedDate.length +
             fantomSeparator +
-            getDateSegmentValueLength(dateString)[segmentName];
+            getDateSegmentValueLength(dateModeTemplate)[segmentName];
         const isLastSegmentDigitAdded =
             lastSegmentDigitIndex >= from && lastSegmentDigitIndex <= to;
 
@@ -48,6 +48,11 @@ export function validateDateString({
             return {validatedDateString: '', updatedSelection: [from, to]}; // prevent changes
         }
 
+        if (isLastSegmentDigitAdded && Number(segmentValue) < 1) {
+            // 31.0|1.2010 => Type 0 => 31.0|1.2010
+            return {validatedDateString: '', updatedSelection: [from, to]}; // prevent changes
+        }
+
         const {validatedSegmentValue, prefixedZeroesCount} = padWithZeroesUntilValid(
             segmentValue,
             `${maxSegmentValue}`,