From 8f9b7857fa7cd9071834b96e8c637c8fb470275e Mon Sep 17 00:00:00 2001 From: tristan Date: Tue, 9 Jun 2026 13:47:17 +0200 Subject: [PATCH] feat(amount) : helpers amountFormat (normalize, group, curseur) --- .../input/composables/amountFormat.test.ts | 74 +++++++++++++++++++ .../malio/input/composables/amountFormat.ts | 40 ++++++++++ 2 files changed, 114 insertions(+) create mode 100644 app/components/malio/input/composables/amountFormat.test.ts create mode 100644 app/components/malio/input/composables/amountFormat.ts diff --git a/app/components/malio/input/composables/amountFormat.test.ts b/app/components/malio/input/composables/amountFormat.test.ts new file mode 100644 index 0000000..484c52e --- /dev/null +++ b/app/components/malio/input/composables/amountFormat.test.ts @@ -0,0 +1,74 @@ +import {describe, expect, it} from 'vitest' +import {normalizeAmount, formatGroupedAmount, countSignificant, caretFromSignificant} from './amountFormat' + +describe('normalizeAmount', () => { + it('garde le point décimal', () => { + expect(normalizeAmount('12.5')).toBe('12.5') + }) + it('convertit la virgule en point et nettoie', () => { + expect(normalizeAmount('0012,345abc')).toBe('12.34') + }) + it('normalise une décimale en tête', () => { + expect(normalizeAmount(',5')).toBe('0.5') + }) + it('retire les espaces', () => { + expect(normalizeAmount('1 234 567')).toBe('1234567') + }) + it('limite à 2 décimales', () => { + expect(normalizeAmount('1234.567')).toBe('1234.56') + }) + it('garde une décimale en cours de saisie', () => { + expect(normalizeAmount('12.')).toBe('12.') + }) + it('renvoie une chaîne vide pour une saisie non numérique', () => { + expect(normalizeAmount('abc')).toBe('') + }) + it('garde un zéro seul', () => { + expect(normalizeAmount('0')).toBe('0') + }) +}) + +describe('formatGroupedAmount', () => { + it('groupe la partie entière par 3 avec des espaces', () => { + expect(formatGroupedAmount('1234567')).toBe('1 234 567') + }) + it('utilise la virgule comme séparateur décimal', () => { + expect(formatGroupedAmount('1234.56')).toBe('1 234,56') + }) + it('affiche une virgule pour une décimale en cours', () => { + expect(formatGroupedAmount('12.')).toBe('12,') + }) + it('gère les valeurs sous 1000 sans séparateur', () => { + expect(formatGroupedAmount('12')).toBe('12') + }) + it('groupe avec une décimale en tête', () => { + expect(formatGroupedAmount('0.5')).toBe('0,5') + }) + it('renvoie une chaîne vide pour une chaîne vide', () => { + expect(formatGroupedAmount('')).toBe('') + }) + it('garde un zéro seul', () => { + expect(formatGroupedAmount('0')).toBe('0') + }) +}) + +describe('countSignificant', () => { + it('compte les caractères hors espaces à gauche du curseur', () => { + expect(countSignificant('1 234', 5)).toBe(4) + }) + it('ignore un espace juste avant le curseur', () => { + expect(countSignificant('1 234', 2)).toBe(1) + }) +}) + +describe('caretFromSignificant', () => { + it('place le curseur après le n-ième caractère significatif', () => { + expect(caretFromSignificant('1 234 567', 4)).toBe(5) + }) + it('place le curseur en fin si on dépasse', () => { + expect(caretFromSignificant('1 234', 10)).toBe(5) + }) + it('place le curseur au début pour 0 caractère significatif', () => { + expect(caretFromSignificant('1 234', 0)).toBe(0) + }) +}) diff --git a/app/components/malio/input/composables/amountFormat.ts b/app/components/malio/input/composables/amountFormat.ts new file mode 100644 index 0000000..0ab574f --- /dev/null +++ b/app/components/malio/input/composables/amountFormat.ts @@ -0,0 +1,40 @@ +// Parse : texte saisi (espaces, virgule, caractères parasites) → chaîne numérique propre. +export const normalizeAmount = (value: string): string => { + const sanitizedValue = value + .replace(/\s+/g, '') + .replace(/,/g, '.') + .replace(/[^\d.]/g, '') + const [integerPartRaw = '', ...decimalParts] = sanitizedValue.split('.') + const integerPart = integerPartRaw.replace(/^0+(?=\d)/, '') + const decimalPart = decimalParts.join('').slice(0, 2) + + if (sanitizedValue.includes('.')) { + return `${integerPart || '0'}.${decimalPart}` + } + + return integerPart +} + +// Format : modèle propre (point décimal) → affichage groupé FR (espaces + virgule). +export const formatGroupedAmount = (model: string): string => { + if (model === '') return '' + const hasDot = model.includes('.') + const [integerPart = '', decimalPart = ''] = model.split('.') + const groupedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + return hasDot ? `${groupedInteger},${decimalPart}` : groupedInteger +} + +// Nombre de caractères significatifs (hors espaces de groupement) à gauche d'une position. +export const countSignificant = (str: string, upTo: number): number => + str.slice(0, upTo).replace(/ /g, '').length + +// Position de curseur après le n-ième caractère significatif dans la chaîne affichée. +export const caretFromSignificant = (display: string, sig: number): number => { + if (sig <= 0) return 0 + let seen = 0 + for (let i = 0; i < display.length; i++) { + if (display[i] !== ' ') seen++ + if (seen >= sig) return i + 1 + } + return display.length +}