Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/add function describeCurrency() #342

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/pt-br/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions docs/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('Public API', () => {
'capitalize',
'formatCurrency',
'parseCurrency',
'describeCurrency'
];

Object.keys(API).forEach((method) => {
Expand Down
130 changes: 129 additions & 1 deletion src/utilities/currency/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,132 @@
import { format, parse } from '.';
import { describe as describeNumber, format, parse, pt } from '.';

describe('describe', () => {
test('should describe irregular numbers', () => {
expect.assertions(pt.irregular.length * 3);
for (let i = 0; i < 20; i++) {
const irregular = pt.irregular[i];
expect(describeNumber(i, false)).toBe(irregular);
expect(describeNumber(i)).toBe(`${irregular} reais e zero centavos`);
expect(describeNumber(i + 0.05)).toBe(`${irregular} reais e cinco centavos`);
}
});

test('should describe rounded ten numbers', () => {
expect.assertions(pt.ten.length * 2);
for (let i = 0; i < 10; i++) {
const n = i * 10;
const ten = pt.irregular[n] ?? pt.ten[i];
expect(describeNumber(n, false)).toBe(ten);
expect(describeNumber(n)).toBe(`${ten} reais e zero centavos`);
}
});

test('should describe composed ten numbers', () => {
expect.assertions((pt.ten.length - 2) * 3);
for (let i = 2; i < 10; i++) {
const n = i * 10;
const singular = pt.irregular[i];
const ten = pt.irregular[n] ?? pt.ten[i];
const composed = ten === singular ? ten : `${ten} e ${singular}`;
expect(describeNumber(n + 0.55)).toBe(`${ten} reais e cinquenta e cinco centavos`);
expect(describeNumber(n + i)).toBe(`${composed} reais e zero centavos`);
expect(describeNumber(n + i + 0.55)).toBe(`${composed} reais e cinquenta e cinco centavos`);
}
});

test('should describe hundred numbers', () => {
expect.assertions((pt.hundred.length - 1) * 7);
for (let i = 1; i < 10; i++) {
const n = i * 100;
const singular = pt.irregular[i];
const hundred = pt.hundred[i];
const singularHundred = pt.hundred[i].replace(/nto$/i, 'm');
expect(describeNumber(n, false)).toBe(singularHundred);
expect(describeNumber(n)).toBe(`${singularHundred} reais e zero centavos`);
expect(describeNumber(n + 0.66)).toBe(`${singularHundred} reais e sessenta e seis centavos`);
expect(describeNumber(n + i)).toBe(`${hundred} e ${singular} reais e zero centavos`);
expect(describeNumber(n + 20)).toBe(`${hundred} e vinte reais e zero centavos`);
expect(describeNumber(n + i + 20)).toBe(`${hundred} e vinte e ${singular} reais e zero centavos`);
expect(describeNumber(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 = describeNumber(i, false);
expect(describeNumber(n, false)).toBe(`${thousand} mil`);
expect(describeNumber(n)).toBe(`${thousand} mil reais e zero centavos`);
expect(describeNumber(n + 0.5)).toBe(`${thousand} mil reais e cinquenta centavos`);
}
});

test('should describe composed thousand numbers', () => {
expect.assertions((pt.hundred.length - 1) * 6);
for (let i = 1; i < 10; i++) {
const n = i * 1000;
const singular = pt.irregular[i];
const thousand = describeNumber(n, false);
expect(describeNumber(n + 0.66)).toBe(`${thousand} reais e sessenta e seis centavos`);
expect(describeNumber(n + i)).toBe(`${thousand} e ${singular} reais e zero centavos`);
expect(describeNumber(n + 20)).toBe(`${thousand} e vinte reais e zero centavos`);
expect(describeNumber(n + 20 + i)).toBe(`${thousand} e vinte e ${singular} reais e zero centavos`);
expect(describeNumber(n + 120 + i)).toBe(`${thousand} cento e vinte e ${singular} reais e zero centavos`);
expect(describeNumber(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 = describeNumber(i, false);
const assignment = i === 1 ? 'milhão' : 'milhões';
expect(describeNumber(n, false)).toBe(`${million} ${assignment}`);
expect(describeNumber(n)).toBe(`${million} ${assignment} de reais e zero centavos`);
expect(describeNumber(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 = pt.irregular[i];
const million = describeNumber(i, false);
const assignment = i === 1 ? 'milhão' : 'milhões';
expect(describeNumber(n + 0.66)).toBe(`${million} ${assignment} de reais e sessenta e seis centavos`);
expect(describeNumber(n + i)).toBe(`${million} ${assignment} e ${singular} reais e zero centavos`);
expect(describeNumber(n + 20)).toBe(`${million} ${assignment} e vinte reais e zero centavos`);
expect(describeNumber(n + 20 + i)).toBe(
`${million} ${assignment} e vinte e ${singular} reais e zero centavos`
);
expect(describeNumber(n + 120 + i)).toBe(
`${million} ${assignment} cento e vinte e ${singular} reais e zero centavos`
);
expect(describeNumber(n + i + 20 + 0.67)).toBe(
`${million} ${assignment} e vinte e ${singular} reais e sessenta e sete centavos`
);
}
});

test('should describe formatted numbers', () => {
expect(describeNumber('R$ 1', false)).toBe('um');
expect(describeNumber('R$ 2,00', false)).toBe('dois');
expect(describeNumber('R$ 10.00')).toBe('dez reais e zero centavos');
expect(describeNumber('R$ 105,01')).toBe('cento e cinco reais e um centavos');
expect(describeNumber('R$ 105,1')).toBe('cento e cinco reais e dez centavos');
expect(describeNumber('R$ 105,10')).toBe('cento e cinco reais e dez centavos');
expect(describeNumber('R$ 1050,25')).toBe('um mil e cinquenta reais e vinte e cinco centavos');
expect(describeNumber('R$ 105000,99')).toBe('cento e cinco mil reais e noventa e nove centavos');
expect(describeNumber('R$ 105000000,00')).toBe('cento e cinco milhões de reais e zero centavos');
});
});

describe('format', () => {
test('should format Currency into BRL', () => {
Expand Down
145 changes: 145 additions & 0 deletions src/utilities/currency/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,148 @@
import { onlyNumbers } from '../../helpers';

export const pt = {
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',
],
};

type CondicionalValue = string | boolean | number | null | undefined;

function conditionalValue<R extends string>(isTruthy: CondicionalValue, result: R): R | undefined {
return isTruthy ? result : undefined;
}

function describeIrregular(digits: string): string {
const index = Number(digits);
return pt.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 = pt.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 = pt.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;
};
Expand Down
2 changes: 1 addition & 1 deletion src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down