num-beauty
Version:
An ultra lightweight module for formatting numbers into human-friendly strings
1 lines • 4.1 kB
JavaScript
import{locales}from"../locales/index.js";export function toAccessibleString(e,t={}){const n=t.locale||"en-US",r=locales[n],s=r.speech||{},o=getDecimalSeparator(n),i=s.minus||"minus",c=s.point||"point";let l=String(e).trim(),u=!1;l.startsWith("-")&&(u=!0,l=l.slice(1).trim());const a=detectCurrency(l,n);let d=a?.code;a&&(l=a.rest.trim());const{unitIndex:h,rest:f}=detectUnitAbbreviation(l,r.units),g=h>0;l=f.trim();const{integerStr:m,decimalStr:p}=normalizeNumberText(l,o);if(!m&&!p)return String(e);let W;const $=m?parseInt(m,10):0;if(g?W=integerAndFractionToWords($,p,n):(W=integerWithScaleToWords($,n),p&&(W+=" "+c+" "+decimalDigitsToWords(p,n))),g){const e=s.units||[["",""]],t=!p&&1===$,[n,r]=e[h]||["",""],o=t?n:r;o&&(W+=" "+o)}if(d&&s.currency&&s.currency[d]){const e=shouldUsePlural($,p),[t,n]=s.currency[d],r=e?n:t;W=`${W}${(g||$>=1e3)&&s.currencyJoiner?s.currencyJoiner:" "}${r}`.trim()}return u?`${i} ${W}`:W}function getDecimalSeparator(e){try{const t=new Intl.NumberFormat(e).formatToParts(1.1),n=t.find(e=>"decimal"===e.type)?.value;return n||"."}catch{return"."}}function normalizeNumberText(e,t){let n=e.replace(/[^0-9,.]/g,"");const r=n.lastIndexOf("."),s=n.lastIndexOf(","),o=(n.match(/\./g)||[]).length,i=(n.match(/,/g)||[]).length;let c=Math.max(r,s);if(-1===c)n=n.replace(/[.,]/g,"");else{const e=n[c],r=n.slice(c+1);if(o+i===1&&e!==t&&/^\d{3}$/.test(r))n=n.replace(/[.,]/g,"");else{const e=n.slice(0,c).replace(/[.,]/g,""),t=n.slice(c+1).replace(/[.,]/g,"");n=e+(t?"."+t:"")}}const[l,u]=n.split(".");return{integerStr:l||"",decimalStr:u||""}}function detectCurrency(e,t){const n=locales[t]?.currencies||{},r=Object.entries(n);for(const[t,n]of r)if("before"===n.position){const r=n.symbol;if(e.startsWith(r))return{code:t,rest:e.slice(r.length).trim()};if(e.startsWith(r+" "))return{code:t,rest:e.slice(r.length+1).trim()}}else{const r=n.symbol;if(e.endsWith(r))return{code:t,rest:e.slice(0,-r.length).trim()};if(e.endsWith(" "+r))return{code:t,rest:e.slice(0,-(r.length+1)).trim()}}}function detectUnitAbbreviation(e,t){let n=e,r=0;const s=e.toLowerCase();for(let o=t.length-1;o>=1;o--){const[i,c]=t[o];for(const t of[i,c]){if(!t)continue;const i=t.toLowerCase();if(s.endsWith(" "+i))return r=o,n=e.slice(0,-(" "+t).length),{unitIndex:r,rest:n};if(s.endsWith(i))return r=o,n=e.slice(0,-t.length),{unitIndex:r,rest:n}}}const o={k:1,K:1,m:2,M:2,b:3,B:3,t:4,T:4},i=s.replace(/.*?([a-zA-Z]+)\s*$/,"$1");if(i&&null!=o[i]){const t=e.slice(e.length-i.length);return n=e.slice(0,-t.length),{unitIndex:o[i],rest:n}}return{unitIndex:0,rest:n}}function shouldUsePlural(e,t){return!(!t||""===t.replace(/0+$/,""))||1!==e}function integerAndFractionToWords(e,t,n){const r=locales[n].speech?.point||"point",s=integerToWordsBelowThousandGroups(e,n);return t?`${s} ${r} ${decimalDigitsToWords(t,n)}`:s}function integerWithScaleToWords(e,t){return 0===e?(locales[t].speech?.small||["zero"])[0]||"zero":integerToWordsBelowThousandGroups(e,t)}function integerToWordsBelowThousandGroups(e,t){const n=(locales[t].speech||{}).units||[["",""],["thousand","thousand"],["million","million"],["billion","billion"]],r=[];let s=0;for(;e>0;){const o=e%1e3;if(o){const e=threeDigitsToWords(o,t);if(0===s)r.unshift(e);else{const i=1===o,[c,l]=n[s]||["",""];if((t.startsWith("pt")||t.startsWith("es"))&&1===s&&i)r.unshift(c||l);else{const t=i?c||l:l||c;r.unshift(e+(t?" "+t:""))}}}e=Math.floor(e/1e3),s++}return r.join(" ")}function threeDigitsToWords(e,t){const n=locales[t].speech||{},r=n.small||[],s=n.tens||[],o=n.rules||{},i=n.hundredsWords,c=n.hundredOneExact;if(e<20)return r[e]||String(e);if(e<100){const t=Math.floor(e/10),n=e%10;if(0===n)return s[t]||String(e);if(o.tensHyphenate)return`${s[t]}-${r[n]}`;const i=o.tensJoiner??" ";return`${s[t]}${i}${r[n]}`}if(100===e&&c)return c;const l=Math.floor(e/100),u=e%100,a=i?.[l]||`${r[l]} ${o.hundredSuffix||"hundred"}`;return 0===u?a:`${a}${o.hundredsJoiner??" "}${threeDigitsToWords(u,t)}`}function decimalDigitsToWords(e,t){const n=(locales[t].speech||{}).small||[],r=[];for(const t of e){const e=t.charCodeAt(0)-48;r.push(n[e]||t)}return r.join(" ")}