From 06afbcb4a2c5c15e2ef9dc81db4309adf01aa8ef Mon Sep 17 00:00:00 2001 From: Nikita Barsukov Date: Tue, 11 Jul 2023 13:47:51 +0300 Subject: [PATCH] fix(kit): `Prefix`/`Postfix` is incompatible if they end/start with the same character (#366) --- .../kit/number/number-prefix-postfix.cy.ts | 378 +++++++++++------- .../src/pages/cypress/cypress.component.ts | 1 + .../demo/src/pages/cypress/cypress.module.ts | 8 +- .../demo/src/pages/cypress/cypress.style.less | 5 + .../src/pages/cypress/cypress.template.html | 10 +- .../3-mirrored-prefix-postfix/component.ts | 20 + .../lib/processors/postfix-postprocessor.ts | 26 +- .../tests/postfix-postprocessor.spec.ts | 35 +- .../lib/utils/find-common-beginning-substr.ts | 13 + projects/kit/src/lib/utils/index.ts | 1 + .../find-common-beginning-substr.spec.ts | 20 + 11 files changed, 360 insertions(+), 157 deletions(-) create mode 100644 projects/demo/src/pages/cypress/cypress.style.less create mode 100644 projects/demo/src/pages/cypress/examples/3-mirrored-prefix-postfix/component.ts create mode 100644 projects/kit/src/lib/utils/find-common-beginning-substr.ts create mode 100644 projects/kit/src/lib/utils/tests/find-common-beginning-substr.spec.ts diff --git a/projects/demo-integrations/cypress/tests/kit/number/number-prefix-postfix.cy.ts b/projects/demo-integrations/cypress/tests/kit/number/number-prefix-postfix.cy.ts index 0c453533d..e24a15630 100644 --- a/projects/demo-integrations/cypress/tests/kit/number/number-prefix-postfix.cy.ts +++ b/projects/demo-integrations/cypress/tests/kit/number/number-prefix-postfix.cy.ts @@ -1,177 +1,267 @@ import {DemoPath} from '@demo/constants'; +import {BROWSER_SUPPORTS_REAL_EVENTS} from '../../../support/constants'; +import {openNumberPage} from './utils'; + describe('Number | Prefix & Postfix', () => { - beforeEach(() => { - cy.visit( - `/${DemoPath.Number}/API?decimalSeparator=.&thousandSeparator=_&precision=2&prefix=$`, - ); - cy.get('#demo-content input').should('be.visible').first().as('input'); - - cy.get('tr') - .contains('[postfix]') - .parents('tr') - .find('tui-primitive-textfield') - .type(' per day'); - - cy.get('@input') - .focus() - .should('have.value', '$ per day') - .should('have.prop', 'selectionStart', 1) - .should('have.prop', 'selectionEnd', 1); - }); + describe('[prefix]="$" | [postfix]=" per day"', () => { + beforeEach(() => { + cy.visit( + `/${DemoPath.Number}/API?decimalSeparator=.&thousandSeparator=_&precision=2&prefix=$`, + ); + cy.get('#demo-content input').should('be.visible').first().as('input'); - describe('thousand separators correctly works with prefix', () => { - it('Type 1000000', () => { - cy.get('@input') - .type('1000000') - .should('have.value', '$1_000_000 per day') - .should('have.prop', 'selectionStart', '$1_000_000'.length) - .should('have.prop', 'selectionEnd', '$1_000_000'.length); - }); + cy.get('tr') + .contains('[postfix]') + .parents('tr') + .find('tui-primitive-textfield') + .type(' per day'); - it('$1_000_000| per day => Backspace => Backspace x2', () => { cy.get('@input') - .type('1000000') - .type('{backspace}') - .should('have.value', '$100_000 per day') - .should('have.prop', 'selectionStart', '$100_000'.length) - .should('have.prop', 'selectionEnd', '$100_000'.length) - .type('{backspace}'.repeat(2)) - .should('have.value', '$1_000 per day') - .should('have.prop', 'selectionStart', '$1_000'.length) - .should('have.prop', 'selectionEnd', '$1_000'.length) - .type('{backspace}') - .should('have.value', '$100 per day') - .should('have.prop', 'selectionStart', '$100'.length) - .should('have.prop', 'selectionEnd', '$100'.length); - }); - - it('$|1_234 per day => Del => $|234 per day', () => { - cy.get('@input') - .type('1234') - .should('have.value', '$1_234 per day') - .should('have.prop', 'selectionStart', '$1_234'.length) - .should('have.prop', 'selectionEnd', '$1_234'.length) - .type('{moveToStart}{del}') - .should('have.value', '$234 per day') + .focus() + .should('have.value', '$ per day') .should('have.prop', 'selectionStart', 1) .should('have.prop', 'selectionEnd', 1); }); - it('$1_2|34 per day => Del => $|234 per day', () => { - cy.get('@input') - .type('1234') - .should('have.value', '$1_234 per day') - .should('have.prop', 'selectionStart', '$1_234'.length) - .should('have.prop', 'selectionEnd', '$1_234'.length) - .type('{leftArrow}{leftArrow}{backspace}') - .should('have.value', '$134 per day') - .should('have.prop', 'selectionStart', '$1'.length) - .should('have.prop', 'selectionEnd', '$1'.length); - }); - }); + describe('thousand separators correctly works with prefix', () => { + it('Type 1000000', () => { + cy.get('@input') + .type('1000000') + .should('have.value', '$1_000_000 per day') + .should('have.prop', 'selectionStart', '$1_000_000'.length) + .should('have.prop', 'selectionEnd', '$1_000_000'.length); + }); - it('pads integer part with zero if user types decimal separator (for empty input)', () => { - cy.get('@input') - .type('.45') - .should('have.value', '$0.45 per day') - .should('have.prop', 'selectionStart', '$0.45'.length) - .should('have.prop', 'selectionEnd', '$0.45'.length); - }); + it('$1_000_000| per day => Backspace => Backspace x2', () => { + cy.get('@input') + .type('1000000') + .type('{backspace}') + .should('have.value', '$100_000 per day') + .should('have.prop', 'selectionStart', '$100_000'.length) + .should('have.prop', 'selectionEnd', '$100_000'.length) + .type('{backspace}'.repeat(2)) + .should('have.value', '$1_000 per day') + .should('have.prop', 'selectionStart', '$1_000'.length) + .should('have.prop', 'selectionEnd', '$1_000'.length) + .type('{backspace}') + .should('have.value', '$100 per day') + .should('have.prop', 'selectionStart', '$100'.length) + .should('have.prop', 'selectionEnd', '$100'.length); + }); - it('precision works', () => { - cy.get('@input') - .type('.12345678') - .should('have.value', '$0.12 per day') - .should('have.prop', 'selectionStart', '$0.12'.length) - .should('have.prop', 'selectionEnd', '$0.12'.length); - }); + it('$|1_234 per day => Del => $|234 per day', () => { + cy.get('@input') + .type('1234') + .should('have.value', '$1_234 per day') + .should('have.prop', 'selectionStart', '$1_234'.length) + .should('have.prop', 'selectionEnd', '$1_234'.length) + .type('{moveToStart}{del}') + .should('have.value', '$234 per day') + .should('have.prop', 'selectionStart', 1) + .should('have.prop', 'selectionEnd', 1); + }); - describe('it removes repeated leading zeroes for integer part', () => { - it('Type 000000 => $0| per day', () => { - cy.get('@input') - .type('000000') - .should('have.value', '$0 per day') - .should('have.prop', 'selectionStart', '$0'.length) - .should('have.prop', 'selectionEnd', '$0'.length); + it('$1_2|34 per day => Del => $|234 per day', () => { + cy.get('@input') + .type('1234') + .should('have.value', '$1_234 per day') + .should('have.prop', 'selectionStart', '$1_234'.length) + .should('have.prop', 'selectionEnd', '$1_234'.length) + .type('{leftArrow}{leftArrow}{backspace}') + .should('have.value', '$134 per day') + .should('have.prop', 'selectionStart', '$1'.length) + .should('have.prop', 'selectionEnd', '$1'.length); + }); }); - it('$0| per day => Type 5 => $5| per day', () => { + it('pads integer part with zero if user types decimal separator (for empty input)', () => { cy.get('@input') - .type('0') - .should('have.value', '$0 per day') - .should('have.prop', 'selectionStart', '$0'.length) - .should('have.prop', 'selectionEnd', '$0'.length) - .type('5') - .should('have.value', '$5 per day') - .should('have.prop', 'selectionStart', '$5'.length) - .should('have.prop', 'selectionEnd', '$5'.length); + .type('.45') + .should('have.value', '$0.45 per day') + .should('have.prop', 'selectionStart', '$0.45'.length) + .should('have.prop', 'selectionEnd', '$0.45'.length); }); - it('$0.|05 per day => Backspace => $|5 per day', () => { + it('precision works', () => { cy.get('@input') - .type('0.05') - .type('{leftArrow}'.repeat('05'.length)) - .type('{backspace}') - .should('have.value', '$5 per day') - .should('have.prop', 'selectionStart', '$'.length) - .should('have.prop', 'selectionEnd', '$'.length); + .type('.12345678') + .should('have.value', '$0.12 per day') + .should('have.prop', 'selectionStart', '$0.12'.length) + .should('have.prop', 'selectionEnd', '$0.12'.length); }); - }); - describe('cannot erase prefix', () => { - it('Select all + Backspace', () => { - cy.get('@input') - .type('{selectAll}{backspace}') - .should('have.value', '$ per day') - .should('have.prop', 'selectionStart', 1) - .should('have.prop', 'selectionEnd', 1); + describe('it removes repeated leading zeroes for integer part', () => { + it('Type 000000 => $0| per day', () => { + cy.get('@input') + .type('000000') + .should('have.value', '$0 per day') + .should('have.prop', 'selectionStart', '$0'.length) + .should('have.prop', 'selectionEnd', '$0'.length); + }); + + it('$0| per day => Type 5 => $5| per day', () => { + cy.get('@input') + .type('0') + .should('have.value', '$0 per day') + .should('have.prop', 'selectionStart', '$0'.length) + .should('have.prop', 'selectionEnd', '$0'.length) + .type('5') + .should('have.value', '$5 per day') + .should('have.prop', 'selectionStart', '$5'.length) + .should('have.prop', 'selectionEnd', '$5'.length); + }); + + it('$0.|05 per day => Backspace => $|5 per day', () => { + cy.get('@input') + .type('0.05') + .type('{leftArrow}'.repeat('05'.length)) + .type('{backspace}') + .should('have.value', '$5 per day') + .should('have.prop', 'selectionStart', '$'.length) + .should('have.prop', 'selectionEnd', '$'.length); + }); }); - it('$|42 per day => Backspace => $|42 per day', () => { - cy.get('@input') - .type('42') - .type('{moveToStart}') - .should('have.prop', 'selectionStart', '$'.length) - .should('have.prop', 'selectionEnd', '$'.length) - .type('{backspace}'.repeat(5)) - .should('have.value', '$42 per day') - .should('have.prop', 'selectionStart', '$'.length) - .should('have.prop', 'selectionEnd', '$'.length); + describe('cannot erase prefix', () => { + it('Select all + Backspace', () => { + cy.get('@input') + .type('{selectAll}{backspace}') + .should('have.value', '$ per day') + .should('have.prop', 'selectionStart', 1) + .should('have.prop', 'selectionEnd', 1); + }); + + it('$|42 per day => Backspace => $|42 per day', () => { + cy.get('@input') + .type('42') + .type('{moveToStart}') + .should('have.prop', 'selectionStart', '$'.length) + .should('have.prop', 'selectionEnd', '$'.length) + .type('{backspace}'.repeat(5)) + .should('have.value', '$42 per day') + .should('have.prop', 'selectionStart', '$'.length) + .should('have.prop', 'selectionEnd', '$'.length); + }); }); - }); - describe('cannot erase postfix', () => { - it('Select all + Delete', () => { - cy.get('@input') - .type('{selectAll}{del}') - .should('have.value', '$ per day') - .should('have.prop', 'selectionStart', 1) - .should('have.prop', 'selectionEnd', 1); + describe('cannot erase postfix', () => { + it('Select all + Delete', () => { + cy.get('@input') + .type('{selectAll}{del}') + .should('have.value', '$ per day') + .should('have.prop', 'selectionStart', 1) + .should('have.prop', 'selectionEnd', 1); + }); + + it('$42| per day => Delete x4 => $42| per day', () => { + cy.get('@input') + .type('42') + .type('{moveToEnd}') + .should('have.prop', 'selectionStart', '$42'.length) + .should('have.prop', 'selectionEnd', '$42'.length) + .type('{del}'.repeat(4)) + .should('have.value', '$42 per day') + .should('have.prop', 'selectionStart', '$42'.length) + .should('have.prop', 'selectionEnd', '$42'.length); + }); }); - it('$42| per day => Delete x4 => $42| per day', () => { - cy.get('@input') - .type('42') - .type('{moveToEnd}') - .should('have.prop', 'selectionStart', '$42'.length) - .should('have.prop', 'selectionEnd', '$42'.length) - .type('{del}'.repeat(4)) - .should('have.value', '$42 per day') - .should('have.prop', 'selectionStart', '$42'.length) - .should('have.prop', 'selectionEnd', '$42'.length); + describe('with maskitoCaretGuard', () => { + it('$|42 per day => ArrowLeft => $|42 per day', () => { + cy.get('@input') + .type('42') + .type('{moveToStart}') + .type('{leftArrow}'.repeat(3)) + .should('have.value', '$42 per day') + .should('have.prop', 'selectionStart', '$'.length) + .should('have.prop', 'selectionEnd', '$'.length); + }); }); }); - describe('with maskitoCaretGuard', () => { - it('$|42 per day => ArrowLeft => $|42 per day', () => { - cy.get('@input') - .type('42') - .type('{moveToStart}') - .type('{leftArrow}'.repeat(3)) - .should('have.value', '$42 per day') - .should('have.prop', 'selectionStart', '$'.length) - .should('have.prop', 'selectionEnd', '$'.length); + describe('prefix/postfix ends/starts with the same character', () => { + describe('[prefix]="$_" | [postfix]="_per_day" (with caret guard)', () => { + beforeEach(() => { + openNumberPage('prefix=$_&postfix=_per_day'); + + cy.get('@input') + .should('have.value', '$__per_day') + .should('have.prop', 'selectionStart', '$_'.length) + .should('have.prop', 'selectionEnd', '$_'.length); + }); + + it('$_|_per_day => Type Backspace => $_|_per_day', () => { + cy.get('@input') + .type('{backspace}') + .should('have.value', '$__per_day') + .should('have.prop', 'selectionStart', '$_'.length) + .should('have.prop', 'selectionEnd', '$_'.length); + }); + + it('$_|_per_day => Type Delete => $_|_per_day', () => { + cy.get('@input') + .type('{del}') + .should('have.value', '$__per_day') + .should('have.prop', 'selectionStart', '$_'.length) + .should('have.prop', 'selectionEnd', '$_'.length); + }); + + it('$_|_per_day => Select all + Delete => $_|_per_day', () => { + cy.get('@input') + .type('{selectAll}{del}') + .should('have.value', '$__per_day') + .should('have.prop', 'selectionStart', '$_'.length) + .should('have.prop', 'selectionEnd', '$_'.length); + }); + }); + + describe('[prefix]="$ " | [postfix]=" per day" (without caret guard)', () => { + beforeEach(() => { + cy.visit(DemoPath.Cypress); + cy.get('#mirrored-prefix-postfix input') + .focus() + .type('{selectAll}{del}') + .should('have.value', '$ per day') + .should('have.prop', 'selectionStart', '$ '.length) + .should('have.prop', 'selectionEnd', '$ '.length) + .as('input'); + }); + + it('$ per day| => Type Backspace => $ per da|y', () => { + cy.get('@input') + .type('{moveToEnd}') + .type('{backspace}') + .should('have.value', '$ per day') + .should('have.prop', 'selectionStart', '$ per da'.length) + .should('have.prop', 'selectionEnd', '$ per da'.length); + }); + + it('$ per da|y => Type Backspace => $ per d|ay', () => { + cy.get('@input') + .type('{moveToEnd}{leftArrow}') + .type('{backspace}') + .should('have.value', '$ per day') + .should('have.prop', 'selectionStart', '$ per d'.length) + .should('have.prop', 'selectionEnd', '$ per d'.length); + }); + + it( + '$ p|er |day => Type Backspace => $ p|er da|y', + BROWSER_SUPPORTS_REAL_EVENTS, + () => { + cy.get('@input') + .type('{moveToEnd}') + .type('{leftArrow}'.repeat('day'.length)) + .realPress(['Shift', ...Array('1.1'.length).fill('ArrowLeft')]); + + cy.get('@input') + .type('{backspace}') + .should('have.value', '$ per day') + .should('have.prop', 'selectionStart', '$ p'.length) + .should('have.prop', 'selectionEnd', '$ p'.length); + }, + ); }); }); }); diff --git a/projects/demo/src/pages/cypress/cypress.component.ts b/projects/demo/src/pages/cypress/cypress.component.ts index 52058a771..ae43c942d 100644 --- a/projects/demo/src/pages/cypress/cypress.component.ts +++ b/projects/demo/src/pages/cypress/cypress.component.ts @@ -3,6 +3,7 @@ import {ChangeDetectionStrategy, Component} from '@angular/core'; @Component({ selector: 'cypress-doc-page', templateUrl: './cypress.template.html', + styleUrls: ['./cypress.style.less'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class CypressDocPageComponent {} diff --git a/projects/demo/src/pages/cypress/cypress.module.ts b/projects/demo/src/pages/cypress/cypress.module.ts index ba400f255..45ae709ad 100644 --- a/projects/demo/src/pages/cypress/cypress.module.ts +++ b/projects/demo/src/pages/cypress/cypress.module.ts @@ -10,6 +10,7 @@ import {TuiInputModule} from '@taiga-ui/kit'; import {CypressDocPageComponent} from './cypress.component'; import {TestDocExample1} from './examples/1-predicate/component'; import {TestDocExample2} from './examples/2-native-max-length/component'; +import {TestDocExample3} from './examples/3-mirrored-prefix-postfix/component'; @NgModule({ imports: [ @@ -21,7 +22,12 @@ import {TestDocExample2} from './examples/2-native-max-length/component'; TuiAddonDocModule, RouterModule.forChild(tuiGenerateRoutes(CypressDocPageComponent)), ], - declarations: [CypressDocPageComponent, TestDocExample1, TestDocExample2], + declarations: [ + CypressDocPageComponent, + TestDocExample1, + TestDocExample2, + TestDocExample3, + ], exports: [CypressDocPageComponent], }) export class CypressDocPageModule {} diff --git a/projects/demo/src/pages/cypress/cypress.style.less b/projects/demo/src/pages/cypress/cypress.style.less new file mode 100644 index 000000000..04b7b08c0 --- /dev/null +++ b/projects/demo/src/pages/cypress/cypress.style.less @@ -0,0 +1,5 @@ +.tests-wrapper { + display: flex; + flex-direction: column; + gap: 3rem; +} diff --git a/projects/demo/src/pages/cypress/cypress.template.html b/projects/demo/src/pages/cypress/cypress.template.html index 69d7efc32..635ff1621 100644 --- a/projects/demo/src/pages/cypress/cypress.template.html +++ b/projects/demo/src/pages/cypress/cypress.template.html @@ -1,7 +1,13 @@ - +
+ - + + + +
diff --git a/projects/demo/src/pages/cypress/examples/3-mirrored-prefix-postfix/component.ts b/projects/demo/src/pages/cypress/examples/3-mirrored-prefix-postfix/component.ts new file mode 100644 index 000000000..68087e81b --- /dev/null +++ b/projects/demo/src/pages/cypress/examples/3-mirrored-prefix-postfix/component.ts @@ -0,0 +1,20 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {MaskitoOptions} from '@maskito/core'; +import {maskitoNumberOptionsGenerator} from '@maskito/kit'; + +@Component({ + selector: 'test-doc-example-3', + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TestDocExample3 { + readonly numberMask: MaskitoOptions = maskitoNumberOptionsGenerator({ + prefix: '$ ', + postfix: ' per day', + }); +} diff --git a/projects/kit/src/lib/processors/postfix-postprocessor.ts b/projects/kit/src/lib/processors/postfix-postprocessor.ts index cfe0e156f..4d3457802 100644 --- a/projects/kit/src/lib/processors/postfix-postprocessor.ts +++ b/projects/kit/src/lib/processors/postfix-postprocessor.ts @@ -1,16 +1,16 @@ import {MaskitoPostprocessor} from '@maskito/core'; -import {identity} from '../utils'; +import {escapeRegExp, findCommonBeginningSubstr, identity} from '../utils'; export function maskitoPostfixPostprocessorGenerator( postfix: string, ): MaskitoPostprocessor { + const postfixRE = new RegExp(`${escapeRegExp(postfix)}$`); + return postfix ? ({value, selection}, initialElementState) => { - if ( - value.endsWith(postfix) || // already valid - (!value && !initialElementState.value.endsWith(postfix)) // cases when developer wants input to be empty - ) { + if (!value && !initialElementState.value.endsWith(postfix)) { + // cases when developer wants input to be empty (programmatically) return {value, selection}; } @@ -21,14 +21,28 @@ export function maskitoPostfixPostprocessorGenerator( return {selection, value: value + postfix}; } + const initialValueBeforePostfix = initialElementState.value.replace( + postfixRE, + '', + ); + const postfixWasModified = + initialElementState.selection[1] >= initialValueBeforePostfix.length; + const alreadyExistedValueBeforePostfix = findCommonBeginningSubstr( + initialValueBeforePostfix, + value, + ); + return { selection, value: Array.from(postfix) .reverse() .reduce((newValue, char, index) => { const i = newValue.length - 1 - index; + const isInitiallyMirroredChar = + alreadyExistedValueBeforePostfix[i] === char && + postfixWasModified; - return newValue[i] !== char + return newValue[i] !== char || isInitiallyMirroredChar ? newValue.slice(0, i + 1) + char + newValue.slice(i + 1) : newValue; }, value), diff --git a/projects/kit/src/lib/processors/tests/postfix-postprocessor.spec.ts b/projects/kit/src/lib/processors/tests/postfix-postprocessor.spec.ts index d34033f2f..fa48f06c5 100644 --- a/projects/kit/src/lib/processors/tests/postfix-postprocessor.spec.ts +++ b/projects/kit/src/lib/processors/tests/postfix-postprocessor.spec.ts @@ -3,10 +3,10 @@ import {maskitoPostfixPostprocessorGenerator} from '../postfix-postprocessor'; describe('maskitoPostfixPostprocessorGenerator', () => { const EMPTY_INPUT = {value: '', selection: [0, 0] as const}; - describe('prefix is a single character', () => { + describe('postfix is a single character', () => { const postprocessor = maskitoPostfixPostprocessorGenerator('%'); - it('does not add prefix if input was initially empty', () => { + it('does not add postfix if input was initially empty', () => { expect(postprocessor(EMPTY_INPUT, EMPTY_INPUT)).toEqual(EMPTY_INPUT); }); @@ -31,10 +31,10 @@ describe('maskitoPostfixPostprocessorGenerator', () => { }); }); - describe('prefix consists of many characters', () => { + describe('postfix consists of many characters', () => { const postprocessor = maskitoPostfixPostprocessorGenerator('.00'); - it('does not add prefix if input was initially empty', () => { + it('does not add postfix if input was initially empty', () => { expect(postprocessor(EMPTY_INPUT, EMPTY_INPUT)).toEqual(EMPTY_INPUT); }); @@ -67,4 +67,31 @@ describe('maskitoPostfixPostprocessorGenerator', () => { ).toEqual({value: '100.00', selection: [4, 4]}); }); }); + + describe('postfix starts with the same character as other part of the value ends', () => { + it('$_100_per_kg => $_|100_|per_kg (select all digits and underscore) => Delete => $_|_per_kg', () => { + const postprocessor = maskitoPostfixPostprocessorGenerator('_per_kg'); + + expect( + postprocessor( + {value: '$_per_kg', selection: [2, 2]}, // after + {value: '$_100_per_kg', selection: ['$_'.length, '$_100_'.length]}, // initial + ), + ).toEqual({value: '$__per_kg', selection: [2, 2]}); + }); + + it('$__100__per_kg => $__|100__|per_kg (select all digits and 2 underscore) => Delete => $__|__per_kg', () => { + const postprocessor = maskitoPostfixPostprocessorGenerator('__per_kg'); + + expect( + postprocessor( + {value: '$__per_kg', selection: [3, 3]}, // after + { + value: '$__100__per_kg', + selection: ['$__'.length, '$__100__'.length], + }, // initial + ), + ).toEqual({value: '$____per_kg', selection: [3, 3]}); + }); + }); }); diff --git a/projects/kit/src/lib/utils/find-common-beginning-substr.ts b/projects/kit/src/lib/utils/find-common-beginning-substr.ts new file mode 100644 index 000000000..64256a7a2 --- /dev/null +++ b/projects/kit/src/lib/utils/find-common-beginning-substr.ts @@ -0,0 +1,13 @@ +export function findCommonBeginningSubstr(a: string, b: string): string { + let res = ''; + + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return res; + } + + res += a[i]; + } + + return res; +} diff --git a/projects/kit/src/lib/utils/index.ts b/projects/kit/src/lib/utils/index.ts index a2abdbab1..93838d748 100644 --- a/projects/kit/src/lib/utils/index.ts +++ b/projects/kit/src/lib/utils/index.ts @@ -9,6 +9,7 @@ export * from './date/segments-to-date'; export * from './date/to-date-string'; export * from './date/validate-date-string'; export * from './escape-reg-exp'; +export * from './find-common-beginning-substr'; export * from './get-focused'; export * from './get-object-from-entries'; export * from './identity'; diff --git a/projects/kit/src/lib/utils/tests/find-common-beginning-substr.spec.ts b/projects/kit/src/lib/utils/tests/find-common-beginning-substr.spec.ts new file mode 100644 index 000000000..92372cb84 --- /dev/null +++ b/projects/kit/src/lib/utils/tests/find-common-beginning-substr.spec.ts @@ -0,0 +1,20 @@ +import {findCommonBeginningSubstr} from '../find-common-beginning-substr'; + +describe('findCommonBeginningSubstr', () => { + it('returns common substring until all characters are equal', () => { + expect(findCommonBeginningSubstr('123_456', '123456')).toBe('123'); + }); + + it('returns empty string if any string is empty', () => { + expect(findCommonBeginningSubstr('123_456', '')).toBe(''); + expect(findCommonBeginningSubstr('', '123_456')).toBe(''); + }); + + it('returns empty string if the first characters are different', () => { + expect(findCommonBeginningSubstr('012345', '123')).toBe(''); + }); + + it('returns the whole string if all characters are equal', () => { + expect(findCommonBeginningSubstr('777999', '777999')).toBe('777999'); + }); +});