PluginProbe
The Innovative Form Builder – IvyForms / 0.8
The Innovative Form Builder – IvyForms v0.8
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / utils / utilNumberFormatter.ts

utilNumberFormatter.ts in The Innovative Form Builder – IvyForms 0.8, at frontend/src/utils/utilNumberFormatter.ts

189 lines 5.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // Utility functions for number formatting and parsing
2
3 export type NumberFormat = 'us_decimal' | 'us_int' | 'eu_decimal' | 'eu_int' | ''
4
5 export const formatWithThousandsDynamic = (n: number, thousandsSep: string) => {
6 const parts = n.toString().split('.')
7 let intPart = parts[0]
8 const decPart = parts[1] ?? ''
9 intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSep)
10 return { intPart, decPart }
11 }
12
13 export const formatNumber = (
14 value: number | null | undefined,
15 numberFormat: NumberFormat = '',
16 ): string => {
17 if (value === null || value === undefined || isNaN(value)) return ''
18 const num = value
19 switch (numberFormat) {
20 case 'us_decimal': {
21 const { intPart, decPart } = formatWithThousandsDynamic(Math.abs(num), ',')
22 return (num < 0 ? '-' : '') + (decPart ? `${intPart}.${decPart}` : intPart)
23 }
24 case 'us_int': {
25 const { intPart } = formatWithThousandsDynamic(Math.trunc(Math.abs(num)), ',')
26 return (num < 0 ? '-' : '') + intPart
27 }
28 case 'eu_decimal': {
29 const { intPart, decPart } = formatWithThousandsDynamic(Math.abs(num), '.')
30 return (num < 0 ? '-' : '') + (decPart ? `${intPart},${decPart}` : intPart)
31 }
32 case 'eu_int': {
33 const { intPart } = formatWithThousandsDynamic(Math.trunc(Math.abs(num)), '.')
34 return (num < 0 ? '-' : '') + intPart
35 }
36 default:
37 return String(num)
38 }
39 }
40
41 export const sanitizeRaw = (raw: string, numberFormat: NumberFormat = ''): string => {
42 if (!raw) return ''
43 let v = raw.replace(/[^0-9,.-]/g, '')
44 v = v.replace(/-/g, (m, idx) => (idx === 0 ? '-' : ''))
45 if (v.length > 1 && v[0] !== '-') {
46 v = v.replace(/-/g, '')
47 }
48 const fmt = numberFormat
49 const isEU = fmt === 'eu_decimal' || fmt === 'eu_int'
50 const allowDecimal = fmt === 'eu_decimal' || fmt === 'us_decimal' || fmt === ''
51 if (isEU) {
52 let seenDecimal = false
53 let out = ''
54 for (const ch of v) {
55 if (ch === ',') {
56 if (!allowDecimal || seenDecimal) {
57 continue
58 }
59 seenDecimal = true
60 out += ch
61 } else if (ch === '.') {
62 // skip thousands dot
63 } else {
64 out += ch
65 }
66 }
67 v = out
68 } else {
69 let seenDecimal = false
70 let out = ''
71 for (const ch of v) {
72 if (ch === '.') {
73 if (!allowDecimal || seenDecimal) {
74 continue
75 }
76 seenDecimal = true
77 out += ch
78 } else if (ch === ',') {
79 // skip thousands comma
80 } else {
81 out += ch
82 }
83 }
84 v = out
85 }
86 return v
87 }
88
89 export const parseToNumber = (
90 raw: string,
91 numberFormat: NumberFormat = '',
92 min: number = -Infinity,
93 max: number = Infinity,
94 ): number | null => {
95 if (!raw) return null
96 const fmt = numberFormat
97 let v = raw.trim()
98 if (fmt === 'eu_decimal' || fmt === 'eu_int') {
99 v = v.replace(/\./g, '')
100 if (fmt === 'eu_decimal') v = v.replace(',', '.')
101 else v = v.split(',')[0]
102 } else if (fmt === 'us_decimal' || fmt === 'us_int') {
103 v = v.replace(/,/g, '')
104 if (fmt === 'us_int') v = v.split('.')[0]
105 }
106 v = v.replace(/[^0-9.+-]/g, '')
107 if (v === '-' || v === '' || v === '.' || v === '-.') return null
108 const num = Number(v)
109 if (isNaN(num)) return null
110 return Math.min(Math.max(num, min), max)
111 }
112
113 export const formatNumberForField = (raw: unknown, fmt: NumberFormat): string => {
114 if (raw === null || raw === undefined || raw === '') return ''
115 let num: number | null = null
116 if (typeof raw === 'number') {
117 num = isNaN(raw) ? null : raw
118 } else if (typeof raw === 'string') {
119 const s = raw.trim()
120 if (s === '') return ''
121 if (/^[0-9]+$/.test(s)) {
122 num = parseFloat(s)
123 } else if (/^[0-9.,-]+$/.test(s)) {
124 let normalized = s
125 const lastComma = s.lastIndexOf(',')
126 const lastDot = s.lastIndexOf('.')
127 if (lastComma !== -1 && lastDot !== -1) {
128 if (lastComma > lastDot) {
129 normalized = s.replace(/\./g, '').replace(',', '.')
130 } else {
131 normalized = s.replace(/,/g, '')
132 }
133 } else if (lastComma !== -1) {
134 normalized = s
135 .replace(/\./g, '')
136 .replace(/,/g, '.')
137 .replace(/(\.)(?=.*\.)/g, '')
138 } else if (lastDot !== -1) {
139 const parts = s.split('.')
140 if (parts.length > 2) {
141 const dec = parts.pop() as string
142 const intPart = parts.join('')
143 normalized = intPart + '.' + dec
144 } else {
145 normalized = s
146 }
147 normalized = normalized.replace(/,/g, '')
148 }
149 const parsed = parseFloat(normalized)
150 num = isNaN(parsed) ? null : parsed
151 } else {
152 return String(raw)
153 }
154 }
155 if (num === null) return ''
156 const absParts = (n: number, decimals = 2) => {
157 const str = n.toFixed(decimals)
158 const [i, d] = str.split('.')
159 return { i, d: d || '' }
160 }
161 const addThousands = (intPart: string, sep: string) =>
162 intPart.replace(/\B(?=(\d{3})+(?!\d))/g, sep)
163 switch (fmt) {
164 case 'us_decimal': {
165 const { i, d } = absParts(Math.abs(num), 2)
166 const intF = addThousands(i, ',')
167 return (num < 0 ? '-' : '') + `${intF}.${d}`
168 }
169 case 'us_int': {
170 const t = Math.trunc(Math.abs(num))
171 const intF = addThousands(t.toString(), ',')
172 return (num < 0 ? '-' : '') + intF
173 }
174 case 'eu_decimal': {
175 const { i, d } = absParts(Math.abs(num), 2)
176 const intF = addThousands(i, '.')
177 return (num < 0 ? '-' : '') + `${intF},${d}`
178 }
179 case 'eu_int': {
180 const t = Math.trunc(Math.abs(num))
181 const intF = addThousands(t.toString(), '.')
182 return (num < 0 ? '-' : '') + intF
183 }
184 default:
185 // Show up to 2 decimals if needed, otherwise integer
186 return num % 1 === 0 ? num.toString() : num.toFixed(2)
187 }
188 }
189