@archerjessop/utilities 2.8.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- const formatInputDisplay=(e,t)=>{const r=parseFloat(e)||0,n=r%1!=0;switch(t){case"currency":return new Intl.NumberFormat("en-US",{style:"currency",currency:"USD",minimumFractionDigits:n?2:0,maximumFractionDigits:2}).format(r);case"percent":return new Intl.NumberFormat("en-US",{minimumFractionDigits:n&&Math.abs(r)>=.1?1:n?3:0,maximumFractionDigits:Math.abs(r)<1?3:2}).format(r)+"%";case"years":return new Intl.NumberFormat("en-US",{minimumFractionDigits:n?1:0,maximumFractionDigits:1}).format(r)+" yrs.";case"months":return new Intl.NumberFormat("en-US",{minimumFractionDigits:0,maximumFractionDigits:0}).format(r)+" mos.";case"number":return new Intl.NumberFormat("en-US",{minimumFractionDigits:n?2:0,maximumFractionDigits:2}).format(r);default:return e}},parseNumericInput=e=>{if(null==e)return 0;const t=e.toString().replace(/[^0-9.-]/g,"");return parseFloat(t)||0},filterNumericInput=(e,t=!0)=>{if(!e)return"";let r=e.toString();if(r=r.replace(/[^0-9.-]/g,""),t){(r.match(/-/g)||[]).length>1?(r=r.replace(/-/g,""),e.startsWith("-")&&(r="-"+r)):r.includes("-")&&!r.startsWith("-")&&(r="-"+r.replace(/-/g,""))}else r=r.replace(/-/g,"");if((r.match(/\./g)||[]).length>1){const e=r.split(".");r=e[0]+"."+e.slice(1).join("")}return r},formatLiveNumber=(e,t="default")=>{if(!e&&0!==e)return"";const r=e.toString(),n=parseFloat(r);if(isNaN(n))return"";const a=n<0,i=Math.abs(n);let s;if(r.includes("."))if("months"===t){s=Math.round(i).toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")}else if("percent"===t||"years"===t){let e=i.toFixed(2);e=e.replace(/\.?0+$/,"");const[t,r]=e.split("."),n=t.replace(/\B(?=(\d{3})+(?!\d))/g,",");s=r?n+"."+r:n}else{const e=i.toFixed(2),[t,r]=e.split(".");s=t.replace(/\B(?=(\d{3})+(?!\d))/g,",")+"."+r}else{s=Math.floor(i).toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")}return a?"-"+s:s},formatLiveInput=(e,t)=>{const r=filterNumericInput(e,!0),n=formatLiveNumber(r,t);if(!n)return"";switch(t){case"currency":return"$ "+n;case"percent":return n+" %";case"years":return n+" yrs.";case"months":return n+" mos.";default:return n}},calculateCursorPosition=(e,t,r)=>{if(r<=0)return 0;if(r>=e.length)return t.length;let n=0;for(let t=0;t<r;t++)","!==e[t]&&n++;let a=0;for(let e=0;e<t.length;e++){if(a===n)return","===t[e]?e+1:e;","!==t[e]&&a++}return t.length},extractNumericValue=(e,t)=>{if(!e)return 0;let r=e.toString();switch(t){case"currency":r=r.replace(/^\$\s*/,"").trim();break;case"percent":r=r.replace(/\s*%$/,"").trim();break;case"years":r=r.replace(/\s*yrs\.$/,"").trim();break;case"months":r=r.replace(/\s*mos\.$/,"").trim()}return r=r.replace(/,/g,""),parseFloat(r)||0};export{calculateCursorPosition,extractNumericValue,filterNumericInput,formatInputDisplay,formatLiveInput,formatLiveNumber,parseNumericInput};
1
+ const formatInputDisplay=(t,e)=>{const r=parseFloat(t)||0,n=r%1!=0;switch(e){case"currency":return new Intl.NumberFormat("en-US",{style:"currency",currency:"USD",minimumFractionDigits:n?2:0,maximumFractionDigits:2}).format(r);case"percent":return new Intl.NumberFormat("en-US",{minimumFractionDigits:n&&Math.abs(r)>=.1?1:n?3:0,maximumFractionDigits:Math.abs(r)<1?3:2}).format(r)+"%";case"years":return new Intl.NumberFormat("en-US",{minimumFractionDigits:n?1:0,maximumFractionDigits:1}).format(r)+" yrs.";case"months":return new Intl.NumberFormat("en-US",{minimumFractionDigits:0,maximumFractionDigits:0}).format(r)+" mos.";case"number":return new Intl.NumberFormat("en-US",{minimumFractionDigits:n?2:0,maximumFractionDigits:2}).format(r);default:return t}},parseNumericInput=t=>{if(null==t)return 0;const e=t.toString().replace(/[^0-9.-]/g,"");return parseFloat(e)||0},filterNumericInput=(t,e=!0)=>{if(!t)return"";let r=t.toString();if(r=r.replace(/[^0-9.-]/g,""),e){if((r.match(/-/g)||[]).length>1){const e=t.toString().trim().startsWith("-");r=r.replace(/-/g,""),e&&(r="-"+r)}else r.includes("-")&&!r.startsWith("-")&&(r="-"+r.replace(/-/g,""))}else r=r.replace(/-/g,"");if((r.match(/\./g)||[]).length>1){const t=r.split(".");r=t[0]+"."+t.slice(1).join("")}return r},formatLiveNumber=(t,e="default")=>{if(!t&&0!==t)return"";const r=t.toString(),n=parseFloat(r);if(isNaN(n))return"";const i=n<0,a=Math.abs(n);let s;if(r.includes("."))if("months"===e){s=Math.round(a).toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")}else if("percent"===e||"years"===e){let t=a.toFixed(2);t=t.replace(/\.?0+$/,"");const[e,r]=t.split("."),n=e.replace(/\B(?=(\d{3})+(?!\d))/g,",");s=r?n+"."+r:n}else{const t=a.toFixed(2),[e,r]=t.split(".");s=e.replace(/\B(?=(\d{3})+(?!\d))/g,",")+"."+r}else{s=Math.floor(a).toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")}return i?"-"+s:s},formatLiveInput=(t,e)=>{const r=filterNumericInput(t,!0),n=formatLiveNumber(r,e);if(!n)return"";switch(e){case"currency":return"$ "+n;case"percent":return n+" %";case"years":return n+" yrs.";case"months":return n+" mos.";default:return n}},calculateCursorPosition=(t,e,r,n)=>{if(r<=0)return 0;if(r>=t.length){if("currency"===n)return e.length;{const t=e.match(/^[0-9,.-]+/);return t?t[0].length:0}}let i=0;for(let e=0;e<r;e++)","!==t[e]&&i++;let a=0;for(let t=0;t<e.length&&("currency"===n||!/[a-zA-Z]/.test(e[t]));t++){if(a===i)return","===e[t]?t+1:t;","!==e[t]&&" "!==e[t]&&a++}if("currency"===n)return e.length;{const t=e.match(/^[0-9,.\s-]+/);return t?Math.min(t[0].length,e.length):0}},extractNumericValue=(t,e)=>{if(!t)return 0;let r=t.toString();switch(e){case"currency":r=r.replace(/^\$\s*/,"").trim();break;case"percent":r=r.replace(/\s*%$/,"").trim();break;case"years":r=r.replace(/\s*yrs\.$/,"").trim();break;case"months":r=r.replace(/\s*mos\.$/,"").trim()}return r=r.replace(/,/g,""),parseFloat(r)||0};export{calculateCursorPosition,extractNumericValue,filterNumericInput,formatInputDisplay,formatLiveInput,formatLiveNumber,parseNumericInput};
2
2
  //# sourceMappingURL=financial-formatting.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"financial-formatting.js","sources":["../../src/formatting/financial-formatting.js"],"sourcesContent":["// Enhanced formatting for input fields\r\nexport const formatInputDisplay = (value, type) => {\r\n const num = parseFloat(value) || 0;\r\n const hasDecimals = num % 1 !== 0;\r\n \r\n switch (type) {\r\n case \"currency\":\r\n return new Intl.NumberFormat(\"en-US\", {\r\n style: \"currency\",\r\n currency: \"USD\",\r\n minimumFractionDigits: hasDecimals ? 2 : 0,\r\n maximumFractionDigits: 2\r\n }).format(num);\r\n \r\n case \"percent\":\r\n return new Intl.NumberFormat(\"en-US\", {\r\n minimumFractionDigits: hasDecimals && Math.abs(num) >= 0.1 ? 1 : hasDecimals ? 3 : 0,\r\n maximumFractionDigits: Math.abs(num) < 1 ? 3 : 2\r\n }).format(num) + \"%\";\r\n \r\n case \"years\":\r\n return new Intl.NumberFormat(\"en-US\", {\r\n minimumFractionDigits: hasDecimals ? 1 : 0,\r\n maximumFractionDigits: 1\r\n }).format(num) + \" yrs.\";\r\n \r\n case \"months\":\r\n return new Intl.NumberFormat(\"en-US\", {\r\n minimumFractionDigits: 0,\r\n maximumFractionDigits: 0\r\n }).format(num) + \" mos.\";\r\n \r\n case \"number\":\r\n return new Intl.NumberFormat(\"en-US\", {\r\n minimumFractionDigits: hasDecimals ? 2 : 0,\r\n maximumFractionDigits: 2\r\n }).format(num);\r\n \r\n default:\r\n return value;\r\n }\r\n};\r\n\r\nexport const parseNumericInput = (value) => {\r\n // Handle null, undefined, or non-string inputs\r\n if (value === null || value === undefined) {\r\n return 0;\r\n }\r\n \r\n // Remove all non-numeric characters except decimal point and negative sign\r\n const cleaned = value.toString().replace(/[^0-9.-]/g, \"\");\r\n return parseFloat(cleaned) || 0;\r\n};\r\n\r\n// Add these functions to the existing file:\r\n\r\nexport const filterNumericInput = (value, allowNegative = true) => {\r\n if (!value) return \"\";\r\n \r\n let filtered = value.toString();\r\n \r\n // Remove any non-numeric characters except decimal and negative\r\n filtered = filtered.replace(/[^0-9.-]/g, \"\");\r\n \r\n // Handle negative sign - only allow at the beginning\r\n if (allowNegative) {\r\n const negativeCount = (filtered.match(/-/g) || []).length;\r\n if (negativeCount > 1) {\r\n filtered = filtered.replace(/-/g, \"\");\r\n if (value.startsWith(\"-\")) {\r\n filtered = \"-\" + filtered;\r\n }\r\n } else if (filtered.includes(\"-\") && !filtered.startsWith(\"-\")) {\r\n filtered = \"-\" + filtered.replace(/-/g, \"\");\r\n }\r\n } else {\r\n filtered = filtered.replace(/-/g, \"\");\r\n }\r\n \r\n // Handle decimal - only allow one\r\n const decimalCount = (filtered.match(/\\./g) || []).length;\r\n if (decimalCount > 1) {\r\n const parts = filtered.split(\".\");\r\n filtered = parts[0] + \".\" + parts.slice(1).join(\"\");\r\n }\r\n \r\n return filtered;\r\n};\r\n\r\nexport const formatLiveNumber = (value, type = \"default\") => {\r\n if (!value && value !== 0) return \"\";\r\n \r\n const originalStr = value.toString();\r\n const num = parseFloat(originalStr);\r\n if (isNaN(num)) return \"\";\r\n \r\n const isNegative = num < 0;\r\n const absNum = Math.abs(num);\r\n const hasDecimal = originalStr.includes(\".\");\r\n \r\n let result;\r\n \r\n if (!hasDecimal) {\r\n // No decimal point in original input - format as integer\r\n const intStr = Math.floor(absNum).toString();\r\n result = intStr.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\r\n } else {\r\n // Decimal point exists in original input\r\n if (type === \"months\") {\r\n // Months: always round to whole number (no decimals)\r\n const rounded = Math.round(absNum);\r\n result = rounded.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\r\n } else if (type === \"percent\" || type === \"years\") {\r\n // Percentages and Years: remove trailing zeros after decimal\r\n let formatted = absNum.toFixed(2); // First get 2 decimal places\r\n formatted = formatted.replace(/\\.?0+$/, ''); // Remove trailing zeros\r\n \r\n // Add commas to integer part\r\n const [intPart, decPart] = formatted.split(\".\");\r\n const commaInt = intPart.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\r\n result = decPart ? commaInt + \".\" + decPart : commaInt;\r\n } else {\r\n // Currency and default: always show exactly 2 decimal places\r\n const twoDecimal = absNum.toFixed(2);\r\n const [intPart, decPart] = twoDecimal.split(\".\");\r\n const commaInt = intPart.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\r\n result = commaInt + \".\" + decPart;\r\n }\r\n }\r\n \r\n return isNegative ? \"-\" + result : result;\r\n};\r\n\r\nexport const formatLiveInput = (value, type) => {\r\n const filtered = filterNumericInput(value, true);\r\n const formatted = formatLiveNumber(filtered, type); // Make sure type is passed here\r\n \r\n if (!formatted) return \"\";\r\n \r\n switch (type) {\r\n case \"currency\":\r\n return \"$ \" + formatted;\r\n case \"percent\":\r\n return formatted + \" %\";\r\n case \"years\":\r\n return formatted + \" yrs.\";\r\n case \"months\":\r\n return formatted + \" mos.\";\r\n case \"number\":\r\n return formatted;\r\n default:\r\n return formatted;\r\n }\r\n};\r\n\r\nexport const calculateCursorPosition = (oldValue, newValue, oldCursor) => {\r\n if (oldCursor <= 0) return 0;\r\n if (oldCursor >= oldValue.length) return newValue.length;\r\n \r\n // Count logical characters before cursor position in old value\r\n let logicalCharsBefore = 0;\r\n for (let i = 0; i < oldCursor; i++) {\r\n if (oldValue[i] !== ',') {\r\n logicalCharsBefore++;\r\n }\r\n }\r\n \r\n // Find position after the same number of logical characters in new value\r\n let charsProcessed = 0;\r\n \r\n for (let i = 0; i < newValue.length; i++) {\r\n // If we've found all the logical characters we need, cursor goes after them\r\n if (charsProcessed === logicalCharsBefore) {\r\n // If current position is a comma, move cursor after it\r\n return newValue[i] === ',' ? i + 1 : i;\r\n }\r\n \r\n // Count non-comma characters\r\n if (newValue[i] !== ',') {\r\n charsProcessed++;\r\n }\r\n }\r\n \r\n return newValue.length;\r\n};\r\n\r\nexport const extractNumericValue = (formattedValue, type) => {\r\n if (!formattedValue) return 0;\r\n \r\n let cleaned = formattedValue.toString();\r\n \r\n switch (type) {\r\n case \"currency\":\r\n cleaned = cleaned.replace(/^\\$\\s*/, \"\").trim();\r\n break;\r\n case \"percent\":\r\n cleaned = cleaned.replace(/\\s*%$/, \"\").trim();\r\n break;\r\n case \"years\":\r\n cleaned = cleaned.replace(/\\s*yrs\\.$/, \"\").trim();\r\n break;\r\n case \"months\":\r\n cleaned = cleaned.replace(/\\s*mos\\.$/, \"\").trim();\r\n break;\r\n }\r\n \r\n cleaned = cleaned.replace(/,/g, \"\");\r\n return parseFloat(cleaned) || 0;\r\n};"],"names":["formatInputDisplay","value","type","num","parseFloat","hasDecimals","Intl","NumberFormat","style","currency","minimumFractionDigits","maximumFractionDigits","format","Math","abs","parseNumericInput","cleaned","toString","replace","filterNumericInput","allowNegative","filtered","match","length","startsWith","includes","parts","split","slice","join","formatLiveNumber","originalStr","isNaN","isNegative","absNum","result","round","formatted","toFixed","intPart","decPart","commaInt","twoDecimal","floor","formatLiveInput","calculateCursorPosition","oldValue","newValue","oldCursor","logicalCharsBefore","i","charsProcessed","extractNumericValue","formattedValue","trim"],"mappings":"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"}
1
+ {"version":3,"file":"financial-formatting.js","sources":["../../src/formatting/financial-formatting.js"],"sourcesContent":["// Enhanced formatting for input fields\r\nexport const formatInputDisplay = (value, type) => {\r\n const num = parseFloat(value) || 0;\r\n const hasDecimals = num % 1 !== 0;\r\n \r\n switch (type) {\r\n case \"currency\":\r\n return new Intl.NumberFormat(\"en-US\", {\r\n style: \"currency\",\r\n currency: \"USD\",\r\n minimumFractionDigits: hasDecimals ? 2 : 0,\r\n maximumFractionDigits: 2\r\n }).format(num);\r\n \r\n case \"percent\":\r\n return new Intl.NumberFormat(\"en-US\", {\r\n minimumFractionDigits: hasDecimals && Math.abs(num) >= 0.1 ? 1 : hasDecimals ? 3 : 0,\r\n maximumFractionDigits: Math.abs(num) < 1 ? 3 : 2\r\n }).format(num) + \"%\";\r\n \r\n case \"years\":\r\n return new Intl.NumberFormat(\"en-US\", {\r\n minimumFractionDigits: hasDecimals ? 1 : 0,\r\n maximumFractionDigits: 1\r\n }).format(num) + \" yrs.\";\r\n \r\n case \"months\":\r\n return new Intl.NumberFormat(\"en-US\", {\r\n minimumFractionDigits: 0,\r\n maximumFractionDigits: 0\r\n }).format(num) + \" mos.\";\r\n \r\n case \"number\":\r\n return new Intl.NumberFormat(\"en-US\", {\r\n minimumFractionDigits: hasDecimals ? 2 : 0,\r\n maximumFractionDigits: 2\r\n }).format(num);\r\n \r\n default:\r\n return value;\r\n }\r\n};\r\n\r\nexport const parseNumericInput = (value) => {\r\n // Handle null, undefined, or non-string inputs\r\n if (value === null || value === undefined) {\r\n return 0;\r\n }\r\n \r\n // Remove all non-numeric characters except decimal point and negative sign\r\n const cleaned = value.toString().replace(/[^0-9.-]/g, \"\");\r\n return parseFloat(cleaned) || 0;\r\n};\r\n\r\n// Add these functions to the existing file:\r\n\r\nexport const filterNumericInput = (value, allowNegative = true) => {\r\n if (!value) return \"\";\r\n \r\n let filtered = value.toString();\r\n \r\n // Remove symbols and text, keep only numbers, decimal, and negative\r\n // This regex keeps numeric chars, decimal point, and negative sign\r\n filtered = filtered.replace(/[^0-9.-]/g, \"\");\r\n \r\n // Handle negative sign - only allow at the beginning\r\n if (allowNegative) {\r\n const negativeCount = (filtered.match(/-/g) || []).length;\r\n if (negativeCount > 1) {\r\n // Remove extra negative signs, keep only the first one if input started with -\r\n const startsWithNegative = value.toString().trim().startsWith(\"-\");\r\n filtered = filtered.replace(/-/g, \"\");\r\n if (startsWithNegative) {\r\n filtered = \"-\" + filtered;\r\n }\r\n } else if (filtered.includes(\"-\") && !filtered.startsWith(\"-\")) {\r\n // Move negative to the front if it's not already there\r\n filtered = \"-\" + filtered.replace(/-/g, \"\");\r\n }\r\n } else {\r\n filtered = filtered.replace(/-/g, \"\");\r\n }\r\n \r\n // Handle decimal - only allow one decimal point\r\n const decimalCount = (filtered.match(/\\./g) || []).length;\r\n if (decimalCount > 1) {\r\n const parts = filtered.split(\".\");\r\n filtered = parts[0] + \".\" + parts.slice(1).join(\"\");\r\n }\r\n \r\n return filtered;\r\n};\r\n\r\nexport const formatLiveNumber = (value, type = \"default\") => {\r\n if (!value && value !== 0) return \"\";\r\n \r\n const originalStr = value.toString();\r\n const num = parseFloat(originalStr);\r\n if (isNaN(num)) return \"\";\r\n \r\n const isNegative = num < 0;\r\n const absNum = Math.abs(num);\r\n const hasDecimal = originalStr.includes(\".\");\r\n \r\n let result;\r\n \r\n if (!hasDecimal) {\r\n // No decimal point in original input - format as integer\r\n const intStr = Math.floor(absNum).toString();\r\n result = intStr.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\r\n } else {\r\n // Decimal point exists in original input\r\n if (type === \"months\") {\r\n // Months: always round to whole number (no decimals)\r\n const rounded = Math.round(absNum);\r\n result = rounded.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\r\n } else if (type === \"percent\" || type === \"years\") {\r\n // Percentages and Years: remove trailing zeros after decimal\r\n let formatted = absNum.toFixed(2); // First get 2 decimal places\r\n formatted = formatted.replace(/\\.?0+$/, ''); // Remove trailing zeros\r\n \r\n // Add commas to integer part\r\n const [intPart, decPart] = formatted.split(\".\");\r\n const commaInt = intPart.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\r\n result = decPart ? commaInt + \".\" + decPart : commaInt;\r\n } else {\r\n // Currency and default: always show exactly 2 decimal places\r\n const twoDecimal = absNum.toFixed(2);\r\n const [intPart, decPart] = twoDecimal.split(\".\");\r\n const commaInt = intPart.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\r\n result = commaInt + \".\" + decPart;\r\n }\r\n }\r\n \r\n return isNegative ? \"-\" + result : result;\r\n};\r\n\r\nexport const formatLiveInput = (value, type) => {\r\n const filtered = filterNumericInput(value, true);\r\n const formatted = formatLiveNumber(filtered, type); // Make sure type is passed here\r\n \r\n if (!formatted) return \"\";\r\n \r\n switch (type) {\r\n case \"currency\":\r\n return \"$ \" + formatted;\r\n case \"percent\":\r\n return formatted + \" %\";\r\n case \"years\":\r\n return formatted + \" yrs.\";\r\n case \"months\":\r\n return formatted + \" mos.\";\r\n case \"number\":\r\n return formatted;\r\n default:\r\n return formatted;\r\n }\r\n};\r\n\r\nexport const calculateCursorPosition = (oldValue, newValue, oldCursor, type) => {\r\n if (oldCursor <= 0) return 0;\r\n if (oldCursor >= oldValue.length) {\r\n // Cursor was at end - handle based on format type\r\n if (type === \"currency\") {\r\n // Currency: cursor can go to the very end\r\n return newValue.length;\r\n } else {\r\n // Percentage, years, months: cursor stays before suffix symbols\r\n const match = newValue.match(/^[0-9,.-]+/);\r\n return match ? match[0].length : 0;\r\n }\r\n }\r\n \r\n // Count logical characters before cursor position in old value\r\n let logicalCharsBefore = 0;\r\n for (let i = 0; i < oldCursor; i++) {\r\n if (oldValue[i] !== ',') {\r\n logicalCharsBefore++;\r\n }\r\n }\r\n \r\n // Find position after the same number of logical characters in new value\r\n let charsProcessed = 0;\r\n \r\n for (let i = 0; i < newValue.length; i++) {\r\n // For non-currency types, stop before suffix symbols\r\n if (type !== \"currency\" && /[a-zA-Z]/.test(newValue[i])) {\r\n break;\r\n }\r\n \r\n // If we've found all the logical characters we need\r\n if (charsProcessed === logicalCharsBefore) {\r\n return newValue[i] === ',' ? i + 1 : i;\r\n }\r\n \r\n // Count non-comma, non-space characters (but for currency, allow $ in count)\r\n if (newValue[i] !== ',' && newValue[i] !== ' ') {\r\n charsProcessed++;\r\n }\r\n }\r\n \r\n // Fallback: find appropriate end position based on type\r\n if (type === \"currency\") {\r\n return newValue.length;\r\n } else {\r\n const match = newValue.match(/^[0-9,.\\s-]+/);\r\n return match ? Math.min(match[0].length, newValue.length) : 0;\r\n }\r\n};\r\n\r\nexport const extractNumericValue = (formattedValue, type) => {\r\n if (!formattedValue) return 0;\r\n \r\n let cleaned = formattedValue.toString();\r\n \r\n switch (type) {\r\n case \"currency\":\r\n cleaned = cleaned.replace(/^\\$\\s*/, \"\").trim();\r\n break;\r\n case \"percent\":\r\n cleaned = cleaned.replace(/\\s*%$/, \"\").trim();\r\n break;\r\n case \"years\":\r\n cleaned = cleaned.replace(/\\s*yrs\\.$/, \"\").trim();\r\n break;\r\n case \"months\":\r\n cleaned = cleaned.replace(/\\s*mos\\.$/, \"\").trim();\r\n break;\r\n }\r\n \r\n cleaned = cleaned.replace(/,/g, \"\");\r\n return parseFloat(cleaned) || 0;\r\n};"],"names":["formatInputDisplay","value","type","num","parseFloat","hasDecimals","Intl","NumberFormat","style","currency","minimumFractionDigits","maximumFractionDigits","format","Math","abs","parseNumericInput","cleaned","toString","replace","filterNumericInput","allowNegative","filtered","match","length","startsWithNegative","trim","startsWith","includes","parts","split","slice","join","formatLiveNumber","originalStr","isNaN","isNegative","absNum","result","round","formatted","toFixed","intPart","decPart","commaInt","twoDecimal","floor","formatLiveInput","calculateCursorPosition","oldValue","newValue","oldCursor","logicalCharsBefore","i","charsProcessed","test","min","extractNumericValue","formattedValue"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archerjessop/utilities",
3
- "version": "2.8.0",
3
+ "version": "3.0.0",
4
4
  "description": "Shared utilities for ArcherJessop property analysis tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",