diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index b375f313..5e9bf39b 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -219,6 +219,21 @@ parseCurrency('10.756,11'); // 10756.11 parseCurrency('R$ 10.59'); // 10.59 ``` +## describeCurrency + +Transforma uma string ou numero para uma string descritiva + +```javascript +import { describeCurrency } from '@brazilian-utils/brazilian-utils'; + +describeCurrency(10); // dez reais e zero centavos +describeCurrency(10.75); // dez reais e setenta e cinco centavos +describeCurrency('10.75'); // dez reais e setenta e cinco centavos +describeCurrency('R$ 10,75'); // dez reais e setenta e cinco centavos +describeCurrency('R$ 10.756', false); // dez mil setecentos e cinquenta e seis +describeCurrency('R$ 10.756,11', false); // dez mil setecentos e cinquenta e seis +``` + ## getStates Retorna todos os estados brasileiros. diff --git a/docs/utilities.md b/docs/utilities.md index 09a40031..176dd05b 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -219,6 +219,21 @@ parseCurrency('10.756,11'); // 10756.11 parseCurrency('R$ 10.59'); // 10.59 ``` +## describeCurrency + +Transforms a string or number to an described string + +```javascript +import { describeCurrency } from '@brazilian-utils/brazilian-utils'; + +describeCurrency(10); // dez reais e zero centavos +describeCurrency(10.75); // dez reais e setenta e cinco centavos +describeCurrency('10.75'); // dez reais e setenta e cinco centavos +describeCurrency('R$ 10,75'); // dez reais e setenta e cinco centavos +describeCurrency('R$ 10.756', false); // dez mil setecentos e cinquenta e seis +describeCurrency('R$ 10.756,11', false); // dez mil setecentos e cinquenta e seis +``` + ## getStates Get all Brazilian states. diff --git a/src/index.test.ts b/src/index.test.ts index f8d4baa0..5939f73a 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -30,6 +30,7 @@ describe('Public API', () => { 'capitalize', 'formatCurrency', 'parseCurrency', + 'describeCurrency' ]; Object.keys(API).forEach((method) => { diff --git a/src/utilities/currency/index.test.ts b/src/utilities/currency/index.test.ts index 99265462..515f6340 100644 --- a/src/utilities/currency/index.test.ts +++ b/src/utilities/currency/index.test.ts @@ -1,4 +1,130 @@ -import { format, parse } from '.'; +import { describe as describeCurrency, format, parse, STRINGS } from '.'; + +describe('describe', () => { + test('should describe irregular numbers', () => { + expect.assertions(STRINGS.irregular.length * 3); + for (let i = 0; i < 20; i++) { + const irregular = STRINGS.irregular[i]; + expect(describeCurrency(i, false)).toBe(irregular); + expect(describeCurrency(i)).toBe(`${irregular} reais e zero centavos`); + expect(describeCurrency(i + 0.05)).toBe(`${irregular} reais e cinco centavos`); + } + }); + + test('should describe rounded ten numbers', () => { + expect.assertions(STRINGS.ten.length * 2); + for (let i = 0; i < 10; i++) { + const n = i * 10; + const ten = STRINGS.irregular[n] ?? STRINGS.ten[i]; + expect(describeCurrency(n, false)).toBe(ten); + expect(describeCurrency(n)).toBe(`${ten} reais e zero centavos`); + } + }); + + test('should describe composed ten numbers', () => { + expect.assertions((STRINGS.ten.length - 2) * 3); + for (let i = 2; i < 10; i++) { + const n = i * 10; + const singular = STRINGS.irregular[i]; + const ten = STRINGS.irregular[n] ?? STRINGS.ten[i]; + const composed = ten === singular ? ten : `${ten} e ${singular}`; + expect(describeCurrency(n + 0.55)).toBe(`${ten} reais e cinquenta e cinco centavos`); + expect(describeCurrency(n + i)).toBe(`${composed} reais e zero centavos`); + expect(describeCurrency(n + i + 0.55)).toBe(`${composed} reais e cinquenta e cinco centavos`); + } + }); + + test('should describe hundred numbers', () => { + expect.assertions((STRINGS.hundred.length - 1) * 7); + for (let i = 1; i < 10; i++) { + const n = i * 100; + const singular = STRINGS.irregular[i]; + const hundred = STRINGS.hundred[i]; + const singularHundred = STRINGS.hundred[i].replace(/nto$/i, 'm'); + expect(describeCurrency(n, false)).toBe(singularHundred); + expect(describeCurrency(n)).toBe(`${singularHundred} reais e zero centavos`); + expect(describeCurrency(n + 0.66)).toBe(`${singularHundred} reais e sessenta e seis centavos`); + expect(describeCurrency(n + i)).toBe(`${hundred} e ${singular} reais e zero centavos`); + expect(describeCurrency(n + 20)).toBe(`${hundred} e vinte reais e zero centavos`); + expect(describeCurrency(n + i + 20)).toBe(`${hundred} e vinte e ${singular} reais e zero centavos`); + expect(describeCurrency(n + i + 40 + 0.67)).toBe( + `${hundred} e quarenta e ${singular} reais e sessenta e sete centavos` + ); + } + }); + + test('should describe rounded thousand numbers', () => { + expect.assertions(100 * 3); + for (let i = 1; i <= 100; i++) { + const n = i * 1000; + const thousand = describeCurrency(i, false); + expect(describeCurrency(n, false)).toBe(`${thousand} mil`); + expect(describeCurrency(n)).toBe(`${thousand} mil reais e zero centavos`); + expect(describeCurrency(n + 0.5)).toBe(`${thousand} mil reais e cinquenta centavos`); + } + }); + + test('should describe composed thousand numbers', () => { + expect.assertions((STRINGS.hundred.length - 1) * 6); + for (let i = 1; i < 10; i++) { + const n = i * 1000; + const singular = STRINGS.irregular[i]; + const thousand = describeCurrency(n, false); + expect(describeCurrency(n + 0.66)).toBe(`${thousand} reais e sessenta e seis centavos`); + expect(describeCurrency(n + i)).toBe(`${thousand} e ${singular} reais e zero centavos`); + expect(describeCurrency(n + 20)).toBe(`${thousand} e vinte reais e zero centavos`); + expect(describeCurrency(n + 20 + i)).toBe(`${thousand} e vinte e ${singular} reais e zero centavos`); + expect(describeCurrency(n + 120 + i)).toBe(`${thousand} cento e vinte e ${singular} reais e zero centavos`); + expect(describeCurrency(n + i + 20 + 0.67)).toBe( + `${thousand} e vinte e ${singular} reais e sessenta e sete centavos` + ); + } + }); + + test('should describe rounded million numbers', () => { + expect.assertions(100 * 3); + for (let i = 1; i <= 100; i++) { + const n = i * 1000000; + const million = describeCurrency(i, false); + const assignment = i === 1 ? 'milhão' : 'milhões'; + expect(describeCurrency(n, false)).toBe(`${million} ${assignment}`); + expect(describeCurrency(n)).toBe(`${million} ${assignment} de reais e zero centavos`); + expect(describeCurrency(n + 0.5)).toBe(`${million} ${assignment} de reais e cinquenta centavos`); + } + }); + + test('should describe composed million numbers', () => { + expect.assertions(9 * 6); + for (let i = 1; i < 10; i++) { + const n = i * 1000000; + const singular = STRINGS.irregular[i]; + const million = describeCurrency(i, false); + const assignment = i === 1 ? 'milhão' : 'milhões'; + expect(describeCurrency(n + 0.66)).toBe(`${million} ${assignment} de reais e sessenta e seis centavos`); + expect(describeCurrency(n + i)).toBe(`${million} ${assignment} e ${singular} reais e zero centavos`); + expect(describeCurrency(n + 20)).toBe(`${million} ${assignment} e vinte reais e zero centavos`); + expect(describeCurrency(n + 20 + i)).toBe(`${million} ${assignment} e vinte e ${singular} reais e zero centavos`); + expect(describeCurrency(n + 120 + i)).toBe( + `${million} ${assignment} cento e vinte e ${singular} reais e zero centavos` + ); + expect(describeCurrency(n + i + 20 + 0.67)).toBe( + `${million} ${assignment} e vinte e ${singular} reais e sessenta e sete centavos` + ); + } + }); + + test('should describe formatted numbers', () => { + expect(describeCurrency('R$ 1', false)).toBe('um'); + expect(describeCurrency('R$ 2,00', false)).toBe('dois'); + expect(describeCurrency('R$ 10.00')).toBe('dez reais e zero centavos'); + expect(describeCurrency('R$ 105,01')).toBe('cento e cinco reais e um centavos'); + expect(describeCurrency('R$ 105,1')).toBe('cento e cinco reais e dez centavos'); + expect(describeCurrency('R$ 105,10')).toBe('cento e cinco reais e dez centavos'); + expect(describeCurrency('R$ 1050,25')).toBe('um mil e cinquenta reais e vinte e cinco centavos'); + expect(describeCurrency('R$ 105000,99')).toBe('cento e cinco mil reais e noventa e nove centavos'); + expect(describeCurrency('R$ 105000000,00')).toBe('cento e cinco milhões de reais e zero centavos'); + }); +}); describe('format', () => { test('should format Currency into BRL', () => { diff --git a/src/utilities/currency/index.ts b/src/utilities/currency/index.ts index b6ed6af5..1f9f1839 100644 --- a/src/utilities/currency/index.ts +++ b/src/utilities/currency/index.ts @@ -1,3 +1,149 @@ +import { onlyNumbers } from '../../helpers'; + +export const STRINGS = { + irregular: [ + 'zero', + 'um', + 'dois', + 'três', + 'quatro', + 'cinco', + 'seis', + 'sete', + 'oito', + 'nove', + 'dez', + 'onze', + 'doze', + 'treze', + 'catorze', + 'quinze', + 'dezesseis', + 'dezessete', + 'dezoito', + 'dezenove', + ], + ten: ['zero', 'dez', 'vinte', 'trinta', 'quarenta', 'cinquenta', 'sessenta', 'setenta', 'oitenta', 'noventa'], + hundred: [ + 'zero', + 'cento', + 'duzentos', + 'trezentos', + 'quatrocentos', + 'quinhentos', + 'seiscentos', + 'setecentos', + 'oitocentos', + 'novecentos', + ], +}; + +function conditionalValue(isTruthy: unknown, result: TResult): TResult | undefined { + return isTruthy ? result : undefined; +} + +function describeIrregular(digits: string): string { + const index = Number(digits); + return STRINGS.irregular[index]; +} + +function describeTen(digits: string): string { + const isIrregular = Number(digits) < 20; + if (isIrregular) return describeIrregular(digits); + + const isRounded = /^\d0$/i.test(digits); + const [tenValue, unitValue] = digits.split(''); + const ten = STRINGS.ten[Number(tenValue)]; + if (isRounded) return ten; + + const unit = describeIrregular(unitValue); + return `${ten} e ${unit}`; +} + +function describeHundred(digits: string): string { + const isRounded = /^\d00$/i.test(digits); + const hundredValue = digits.slice(0, 1); + const hundred = STRINGS.hundred[Number(hundredValue)]; + if (isRounded) return hundred.replace(/nto$/i, 'm'); + + const tenValue = digits.slice(1); + const ten = describeTen(tenValue); + return `${hundred} e ${ten}`; +} + +function describeThousand(digits: string): string { + const isRounded = /^\d{1,3}000$/i.test(digits); + const thousandValue = digits.replace(/\d{3}$/i, ''); + const thousand = switchNumber(thousandValue); + if (isRounded) return `${thousand} mil`; + + const hundredValue = digits.replace(/.+(\d{3})$/i, '$1'); + const hundred = switchNumber(Number(hundredValue)); + const goesHundred = Number(hundredValue) > 0; + const goesAnd = goesHundred && Number(hundredValue) <= 100; + return [thousand, ' mil', conditionalValue(goesAnd, ' e'), conditionalValue(goesHundred, ` ${hundred}`)].join(''); +} + +function describeMillion(digits: string): string { + const isRounded = /^\d{1,3}000000$/i.test(digits); + const millionValue = digits.replace(/\d{6}$/i, ''); + const assignment = Number(millionValue) === 1 ? 'milhão' : 'milhões'; + const million = switchNumber(millionValue); + if (isRounded) return `${million} ${assignment}`; + + const thousandValue = digits.replace(/.+(\d{6})$/i, '$1'); + const thousand = switchNumber(Number(thousandValue)); + const goesThousand = Number(thousandValue) > 0; + const goesAnd = goesThousand && Number(thousandValue) <= 100; + return [ + million, + ` ${assignment}`, + conditionalValue(goesAnd, ' e'), + conditionalValue(goesThousand, ` ${thousand}`), + ].join(''); +} + +function switchNumber(value: string | number): string { + const digits = onlyNumbers(value); + + const isMillion = /^\d{7,9}$/i.test(digits); + if (isMillion) return describeMillion(digits); + + const isThousand = /^\d{4,6}$/i.test(digits); + if (isThousand) return describeThousand(digits); + + const isHundred = /^\d{3}$/i.test(digits); + if (isHundred) return describeHundred(digits); + + const isTen = /^\d{2}$/i.test(digits); + if (isTen) return describeTen(digits); + + return describeIrregular(digits); +} + +export function describe(value: string | number, isCash: boolean = true): string { + if (typeof value === 'number') value = String(value); + + let centavos = ''; + const centavosExpression = /[,.]\d{1,2}$/i; + const hasCentavos = centavosExpression.test(value); + if (isCash) { + if (hasCentavos) { + const centavosValue = value.replace(/.+[,.](\d{1,2})$/i, '$1').padEnd(2, '0'); + centavos = `${switchNumber(centavosValue)} centavos`; + } else { + centavos = 'zero centavos'; + } + } + + const reaisValue = value.replace(centavosExpression, ''); + const reaisString = switchNumber(reaisValue); + const goesOf = /milh.{2,3}$/i.test(reaisString); + const reais = [reaisString, conditionalValue(goesOf && isCash, ' de'), conditionalValue(isCash, ' reais')].join(''); + + return [reais, conditionalValue(centavos, ` e ${centavos}`)].join(''); +} + type FormatOptions = { precision?: number; }; diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 48fdf9e9..0cb3589f 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -5,7 +5,7 @@ export { isValid as isValidEmail } from './email'; export { format as formatProcessoJuridico, isValid as isValidProcessoJuridico } from './processo-juridico'; export { format as formatCEP, isValid as isValidCEP } from './cep'; export { format as formatBoleto, isValid as isValidBoleto } from './boleto'; -export { format as formatCurrency, parse as parseCurrency } from './currency'; +export { format as formatCurrency, parse as parseCurrency, describe as describeCurrency } from './currency'; export { format as formatCPF, generate as generateCPF, isValid as isValidCPF } from './cpf'; export { format as formatCNPJ, generate as generateCNPJ, isValid as isValidCNPJ } from './cnpj'; export { capitalize } from './capitalize';