import slug from 'slug'; import { conformToMask } from '@biproxi/react-text-mask'; import createNumberMask from 'text-mask-addons/dist/createNumberMask'; const capitalize = (value: string) => value.toString().charAt(0).toUpperCase() + value.substring(1, value.length).toLowerCase(); const pluralize = (singular: string, plural: string, count: number) => (count === 1 ? singular : plural); const lowerCaseTrim = (value: string) => value.toLowerCase().trim(); const parseGoogleErrorMessage = (value: string) => { /** Collapse google errors pertaining to invalid number (too short, invalid format, etc.) to one message */ if (value.includes('INVALID_PHONE_NUMBER')) { return 'Invalid phone number'; } return `${value.charAt(0).toUpperCase()}${value.slice(1).toLowerCase()}`.split('_').join(' '); }; /** * Formats US/CA phone numbers to the form +1 (xxx) xxx-xxxxx. * Returns international number as is. * Returns null if no input phone number to prevent errors. */ const formatPhoneNumber = (phoneNumber: string) => { if (!phoneNumber) return null; const cleaned = (`${phoneNumber}`).replace(/\D/g, ''); const match = cleaned.match(/^(1|)?(\d{3})(\d{3})(\d{4})$/); if (match) { const intlCode = (match[1] ? '+1 ' : ''); return [intlCode, '(', match[2], ') ', match[3], '-', match[4]].join(''); } return phoneNumber; }; /** * Removes everything except the digits for a phone number. * Returns null if no input phone number to prevent errors. */ const unmaskPhoneNumber = (phoneNumber: string) => { if (!phoneNumber) return null; return phoneNumber.replace(/[^0-9]/g, ''); }; /** * Parses to an integer. */ const unmaskNumber = (maskedValue: string): number => { maskedValue = maskedValue.replace(/\./g, '').replace(/[,]/g, ''); return parseInt(maskedValue, 10); }; /** * Pass in number n and get the ordinals added to it as a string. * For example: 1 -> 1st, 2-> 2nd, 3 -> 3rd, etc * Also works for negative numbers. */ const addNumberOrdinals = (n: number): string => { const s = ['th', 'st', 'nd', 'rd']; const v = n % 100; return n + (s[(v - 20) % 10] || s[v] || s[0]); }; /** * The mask configuration to format a number with * commas and decimals */ const numberMask = createNumberMask({ prefix: '', suffix: '', includeThousandsSeparator: true, thousandsSeparatorSymbol: ',', allowDecimal: true, decimalSymbol: '.', decimalLimit: 10, integerLimit: 10, allowNegative: true, allowLeadingZeroes: true, }); /** * The mask configuration to format a coordinate. * Allow only 3 integers and 10 decimals * */ const coordinateMask = createNumberMask({ prefix: '', suffix: '', includeThousandsSeparator: false, allowDecimal: true, decimalSymbol: '.', decimalLimit: 12, integerLimit: 3, allowNegative: true, allowLeadingZeroes: false, }); /** * Passed in string must be a valid int or float and * a nicely formatted price will be returned. */ const formatNumber = (number: string | number): string => { if (number === undefined || number === null) return ''; return parseFloat(conformToMask(number.toString(), numberMask, { guide: false }).conformedValue).toFixed(2); }; const formatCoordinate = (coordinate: string | number): string => { if (coordinate === undefined || coordinate === null) return ''; return conformToMask(coordinate.toString(), coordinateMask, { guide: false }).conformedValue; }; /** * Passed in string must be a valid int or float and * a condenese format will be returned like 1.2M or 500K */ const condenseNumber = (number: string | number, digits = 1): string => { number = parseInt(number.toString(), 10); const lookup = [ { value: 1, symbol: '' }, { value: 1e3, symbol: 'k' }, { value: 1e6, symbol: 'M' }, { value: 1e9, symbol: 'G' }, { value: 1e12, symbol: 'T' }, { value: 1e15, symbol: 'P' }, { value: 1e18, symbol: 'E' }, ]; const rx = /\.0+$|(\.[0-9]*[1-9])0+$/; const item = lookup.slice().reverse().find((item) => { return number >= item.value; }); return item ? (number / item.value).toFixed(digits).replace(rx, '$1') + item.symbol : '0'; }; /** Pass in a notification from the settings page and format it in line with backend schema * This allows us to use the enum to both present the notification one way and still submit * to the backend in line with models. */ const formatSettingsNotification = (notification: string): string => { const formattedNotification = `disable${notification.slice(0).replace(/ /g, '')}`; return formattedNotification; }; const StringUtil = { capitalize, pluralize, lowerCaseTrim, slug, parseGoogleErrorMessage, formatPhoneNumber, unmaskPhoneNumber, unmaskNumber, numberMask, formatNumber, coordinateMask, formatCoordinate, condenseNumber, addNumberOrdinals, formatSettingsNotification, }; export default StringUtil;