# frozen_string_literal: true require 'extensobr/version' require 'settings' # ----- CARREGA CONFIGURAÇÃO ----- Settings.load_extensobr_settings # ----- CARREGA CORE EXTENSÕES ----- if Settings.extensobr_settings[:use_core_exts] == 'true' require 'core_exts/float' require 'core_exts/integer' require 'core_exts/string' end class Extenso BRL = { delimiter: '.', separator: ',', unit: 'R$', precision: 2, position: 'before' }.freeze unless Extenso.const_defined?('BRL') NUM_SING = 0 unless Extenso.const_defined?('NUM_SING') NUM_PLURAL = 1 unless Extenso.const_defined?('NUM_PLURAL') POS_GENERO = 2 unless Extenso.const_defined?('POS_GENERO') GENERO_MASC = 0 unless Extenso.const_defined?('GENERO_MASC') GENERO_FEM = 1 unless Extenso.const_defined?('GENERO_FEM') VALOR_MAXIMO = 999_999_999 unless Extenso.const_defined?('VALOR_MAXIMO') # As unidades 1 e 2 variam em gênero, pelo que precisamos de dois conjuntos de strings (masculinas e femininas) para as unidades UNIDADES = { GENERO_MASC => { 1 => 'Um', 2 => 'Dois', 3 => 'Três', 4 => 'Quatro', 5 => 'Cinco', 6 => 'Seis', 7 => 'Sete', 8 => 'Oito', 9 => 'Nove' }, GENERO_FEM => { 1 => 'Uma', 2 => 'Duas', 3 => 'Três', 4 => 'Quatro', 5 => 'Cinco', 6 => 'Seis', 7 => 'Sete', 8 => 'Oito', 9 => 'Nove' } }.freeze unless Extenso.const_defined?('UNIDADES') DE11A19 = { 11 => 'Onze', 12 => 'Doze', 13 => 'Treze', 14 => 'Quatorze', 15 => 'Quinze', 16 => 'Dezesseis', 17 => 'Dezessete', 18 => 'Dezoito', 19 => 'Dezenove' }.freeze unless Extenso.const_defined?('DE11A19') DEZENAS = { 10 => 'Dez', 20 => 'Vinte', 30 => 'Trinta', 40 => 'Quarenta', 50 => 'Cinquenta', 60 => 'Sessenta', 70 => 'Setenta', 80 => 'Oitenta', 90 => 'Noventa' }.freeze unless Extenso.const_defined?('DEZENAS') CENTENA_EXATA = 'Cem' unless Extenso.const_defined?('CENTENA_EXATA') # As centenas, com exceção de 'cento', também variam em gênero. Aqui também se faz # necessário dois conjuntos de strings (masculinas e femininas). CENTENAS = { GENERO_MASC => { 100 => 'Cento', 200 => 'Duzentos', 300 => 'Trezentos', 400 => 'Quatrocentos', 500 => 'Quinhentos', 600 => 'Seiscentos', 700 => 'Setecentos', 800 => 'Oitocentos', 900 => 'Novecentos' }, GENERO_FEM => { 100 => 'Cento', 200 => 'Duzentas', 300 => 'Trezentas', 400 => 'Quatrocentas', 500 => 'Quinhentas', 600 => 'Seiscentas', 700 => 'Setecentas', 800 => 'Oitocentas', 900 => 'Novecentas' } }.freeze unless Extenso.const_defined?('CENTENAS') # 'Mil' é invariável, seja em gênero, seja em número MILHAR = 'mil' unless Extenso.const_defined?('MILHAR') MILHOES = { NUM_SING => 'milhão', NUM_PLURAL => 'milhões' }.freeze unless Extenso.const_defined?('MILHOES') UNIDADES_ORDINAL = { GENERO_MASC => { 1 => 'Primeiro', 2 => 'Segundo', 3 => 'Terceiro', 4 => 'Quarto', 5 => 'Quinto', 6 => 'Sexto', 7 => 'Sétimo', 8 => 'Oitavo', 9 => 'Nono' }, GENERO_FEM => { 1 => 'Primeira', 2 => 'Segunda', 3 => 'Terceira', 4 => 'Quarta', 5 => 'Quinta', 6 => 'Sexta', 7 => 'Sétima', 8 => 'Oitava', 9 => 'Nona' } }.freeze unless Extenso.const_defined?('UNIDADES_ORDINAL') DEZENAS_ORDINAL = { GENERO_MASC => { 10 => 'Décimo', 20 => 'Vigésimo', 30 => 'Trigésimo', 40 => 'Quadragésimo', 50 => 'Quinquagésimo', 60 => 'Sexagésimo', 70 => 'Septuagésimo', 80 => 'Octogésimo', 90 => 'Nonagésimo' }, GENERO_FEM => { 10 => 'Décima', 20 => 'Vigésima', 30 => 'Trigésima', 40 => 'Quadragésima', 50 => 'Quinquagésima', 60 => 'Sexagésima', 70 => 'Septuagésima', 80 => 'Octogésima', 90 => 'Nonagésima' } }.freeze unless Extenso.const_defined?('DEZENAS_ORDINAL') CENTENAS_ORDINAL = { GENERO_MASC => { 100 => 'Centésimo', 200 => 'Ducentésimo', 300 => 'Trecentésimo', 400 => 'Quadringentésimo', 500 => 'Quingentésimo', 600 => 'Seiscentésimo', 700 => 'Septingentésimo', 800 => 'Octingentésimo', 900 => 'Noningentésimo' }, GENERO_FEM => { 100 => 'Centésima', 200 => 'Ducentésima', 300 => 'Trecentésima', 400 => 'Quadringentésima', 500 => 'Quingentésima', 600 => 'Seiscentésima', 700 => 'Septingentésima', 800 => 'Octingentésima', 900 => 'Noningentésima' } }.freeze unless Extenso.const_defined?('CENTENAS_ORDINAL') MILHAR_ORDINAL = { GENERO_MASC => { 1000 => 'Milésimo' }, GENERO_FEM => { 1000 => 'Milésima' } }.freeze unless Extenso.const_defined?('MILHAR_ORDINAL') def self.int?(payload) !Integer(payload).nil? rescue StandardError false end def self.float?(payload) !!Float(payload) rescue StandardError false end ####################################################################################################################################### def self.numero(valor, genero = GENERO_MASC) # Gera a representação por extenso de um número inteiro, maior que zero e menor ou igual a VALOR_MAXIMO. # # PARÂMETROS: # valor (Integer) O valor numérico cujo extenso se deseja gerar # # genero (Integer) [Opcional; valor padrão: Extenso::GENERO_MASC] O gênero gramatical (Extenso::GENERO_MASC ou Extenso::GENERO_FEM) # do extenso a ser gerado. Isso possibilita distinguir, por exemplo, entre 'duzentos e dois homens' e 'duzentas e duas mulheres'. # # VALOR DE RETORNO: # (String) O número por extenso # ----- VALIDAÇÃO DOS PARÂMETROS DE ENTRADA ---- if valor.nil? || valor == '' raise "[Exceção em Extenso.numero] Parâmetro 'valor' é nulo" if Settings&.extensobr_settings&.dig(:raise_for_nil) == 'true' return 'Zero' end unless int?(valor) && !valor.nil? "[Exceção em Extenso.numero] Parâmetro 'valor' não é numérico (recebido: '#{valor}')" end if valor <= 0 'Zero' elsif valor > VALOR_MAXIMO raise "[Exceção em Extenso.numero] Parâmetro '#{valor} deve ser um inteiro entre 1 e #{VALOR_MAXIMO} (recebido: '#{valor}')" elsif genero != GENERO_MASC && genero != GENERO_FEM raise "Exceção em Extenso: valor incorreto para o parâmetro 'genero' (recebido: '#{genero}')" # ------------------------------------------------ elsif valor >= 1 && valor <= 9 UNIDADES[genero][valor] elsif valor == 10 DEZENAS[valor] elsif valor >= 11 && valor <= 19 DE11A19[valor] elsif valor >= 20 && valor <= 99 dezena = valor - (valor % 10) ret = DEZENAS[dezena] # Chamada recursiva à função para processar resto se este for maior que zero. # O conectivo 'e' é utilizado entre dezenas e unidades. resto = valor - dezena ret += " e #{numero(resto, genero)}" if resto.positive? ret elsif valor == 100 CENTENA_EXATA elsif valor >= 101 && valor <= 999 centena = valor - (valor % 100) ret = CENTENAS[genero][centena] # As centenas (exceto 'cento') variam em gênero # Chamada recursiva à função para processar resto se este for maior que zero. # O conectivo 'e' é utilizado entre centenas e dezenas. resto = valor - centena ret += " e #{numero(resto, genero)}" if resto.positive? ret elsif valor >= 1000 && valor <= 999_999 # A função 'floor' é utilizada para encontrar o inteiro da divisão de valor por 1000, # assim determinando a quantidade de milhares. O resultado é enviado a uma chamada recursiva # da função. A palavra 'mil' não se flexiona. milhar = (valor / 1000).floor ret = "#{numero(milhar, GENERO_MASC)} #{MILHAR}" # 'Mil' é do gênero masculino resto = valor % 1000 # Chamada recursiva à função para processar resto se este for maior que zero. # O conectivo 'e' é utilizado entre milhares e números entre 1 e 99, bem como antes de centenas exatas. if resto.positive? && ((resto >= 1 && resto <= 99) || (resto % 100).zero?) ret += " e #{numero(resto, genero)}" # Nos demais casos, após o milhar é utilizada a vírgula. elsif resto.positive? ret += ", #{numero(resto, genero)}" end ret elsif valor >= 100_000 && valor <= VALOR_MAXIMO # A função 'floor' é utilizada para encontrar o inteiro da divisão de valor por 1000000, # assim determinando a quantidade de milhões. O resultado é enviado a uma chamada recursiva # da função. A palavra 'milhão' flexiona-se no plural. milhoes = (valor / 1_000_000).floor ret = "#{numero(milhoes, GENERO_MASC)} " # Milhão e milhões são do gênero masculino # Se a o número de milhões for maior que 1, deve-se utilizar a forma flexionada no plural ret += milhoes == 1 ? MILHOES[NUM_SING] : MILHOES[NUM_PLURAL] resto = valor % 1_000_000 # Chamada recursiva à função para processar resto se este for maior que zero. # O conectivo 'e' é utilizado entre milhões e números entre 1 e 99, bem como antes de centenas exatas. if resto && (resto >= 1 && resto <= 99) ret += " e #{numero(resto, genero)}" # Nos demais casos, após o milhão é utilizada a vírgula. elsif resto.positive? ret += ", #{numero(resto, genero)}" end ret end end ####################################################################################################################################### def self.moeda( valor, casas_decimais = 2, info_unidade = ['Real', 'Reais', GENERO_MASC], info_fracao = ['Centavo', 'Centavos', GENERO_MASC] ) # Gera a representação por extenso de um valor monetário, maior que zero e menor ou igual a Extenso::VALOR_MAXIMO. # # # PARÂMETROS: # valor (Float) O valor monetário cujo extenso se deseja gerar. # casas_decimais (Integer) [Opcional; valor padrão: 2] Número de casas decimais a serem consideradas como parte fracionária (centavos) # # info_unidade (Array) [Opcional; valor padrão: ['real', 'reais', Extenso::GENERO_MASC]] Fornece informações sobre a moeda a ser # utilizada. O primeiro valor da matriz corresponde ao nome da moeda no singular, o segundo ao nome da moeda no plural e o terceiro # ao gênero gramatical do nome da moeda (Extenso::GENERO_MASC ou Extenso::GENERO_FEM) # # info_fracao (Array) [Opcional; valor padrão: ['centavo', 'centavos', Extenso::GENERO_MASC]] Provê informações sobre a parte fracionária # da moeda. O primeiro valor da matriz corresponde ao nome da parte fracionária no singular, o segundo ao nome da parte fracionária no plural # e o terceiro ao gênero gramatical da parte fracionária (Extenso::GENERO_MASC ou Extenso::GENERO_FEM) # # VALOR DE RETORNO: # (String) O valor monetário por extenso # ----- VALIDAÇÃO DOS PARÂMETROS DE ENTRADA ---- if valor.nil? || valor == '' raise "[Exceção em Extenso.moeda] Parâmetro 'valor' é nulo" if Settings&.extensobr_settings&.dig(:raise_for_nil) == 'true' return 'Zero Centavos' end unless float?(valor.to_f.round(casas_decimais).to_s) raise "[Exceção em Extenso.moeda] Parâmetro 'valor' não é numérico (recebido: '#{valor}')" end if valor <= 0 'Zero' elsif !int?(casas_decimais) || casas_decimais.negative? raise "[Exceção em Extenso.moeda] Parâmetro 'casas_decimais' não é numérico ou é menor que zero (recebido: '#{casas_decimais}')" elsif info_unidade.class != Array || info_unidade.length < 3 temp = info_unidade.instance_of?(Array) ? "[#{info_unidade.join(', ')}]" : "'#{info_unidade}'" raise "[Exceção em Extenso.moeda] Parâmetro 'info_unidade' não é uma matriz com 3 (três) elementos (recebido: #{temp})" elsif info_unidade[POS_GENERO] != GENERO_MASC && info_unidade[POS_GENERO] != GENERO_FEM raise "Exceção em Extenso: valor incorreto para o parâmetro 'info_unidade[POS_GENERO]' (recebido: '#{info_unidade[POS_GENERO]}')" elsif info_fracao.class != Array || info_fracao.length < 3 temp = info_fracao.instance_of?(Array) ? "[#{info_fracao.join(', ')}]" : "'#{info_fracao}'" raise "[Exceção em Extenso.moeda] Parâmetro 'info_fracao' não é uma matriz com 3 (três) elementos (recebido: #{temp})" elsif info_fracao[POS_GENERO] != GENERO_MASC && info_fracao[POS_GENERO] != GENERO_FEM raise "[Exceção em Extenso.moeda] valor incorreto para o parâmetro 'info_fracao[POS_GENERO]' (recebido: '#{info_fracao[POS_GENERO]}')." end # ----------------------------------------------- ret = '' valor = format("%#{casas_decimais.to_f / 100}f", valor) # A parte inteira do valor monetário corresponde ao valor passado antes do '.' no tipo float. parte_inteira = valor.split('.')[0].to_i # A parte fracionária ('centavos'), por seu turno, corresponderá ao valor passado depois do '.' fracao = valor.to_s.split('.')[1].to_i # os préstimos do método Extenso::numero(). if parte_inteira.positive? ret = numero(parte_inteira, info_unidade[POS_GENERO]) + (parte_inteira >= 1_000_000 && (parte_inteira.to_s.chars.reverse[5] == '0') ? ' de ' : ' ') ret += parte_inteira == 1 ? info_unidade[NUM_SING] : info_unidade[NUM_PLURAL] ret end # De forma semelhante, o extenso da fracao somente será gerado se esta for maior que zero. */ if fracao.positive? # Se a parte_inteira for maior que zero, o extenso para ela já terá sido gerado. Antes de juntar os # centavos, precisamos colocar o conectivo 'e'. ret += ' e ' if parte_inteira.positive? ret += "#{numero(fracao, info_fracao[POS_GENERO])} " ret += fracao == 1 ? info_fracao[NUM_SING] : info_fracao[NUM_PLURAL] end if valor.to_f.zero? ret += "#{numero(fracao, info_fracao[POS_GENERO])} " ret += parte_inteira == 1 ? info_fracao[NUM_SING] : info_fracao[NUM_PLURAL] end ret end ###################################################################################################################################################### def self.ordinal(valor, genero = GENERO_MASC) # Gera a representação ordinal de um número inteiro de 1 à 1000 # PARÂMETROS: # valor (Integer) O valor numérico cujo extenso se deseja gerar # # genero (Integer) [Opcional; valor padrão: Extenso::GENERO_MASC] O gênero gramatical (Extenso::GENERO_MASC ou Extenso::GENERO_FEM) # do extenso a ser gerado. Isso possibilita distinguir, por exemplo, entre 'duzentos e dois homens' e 'duzentas e duas mulheres'. # # VALOR DE RETORNO: # (String) O número por extenso # ----- VALIDAÇÃO DOS PARÂMETROS DE ENTRADA ---- if valor.nil? || valor == '' raise "[Exceção em Extenso.ordinal] Parâmetro 'valor' é nulo" if Settings&.extensobr_settings&.dig(:raise_for_nil) == 'true' return 'Zero' end raise "[Exceção em Extenso.numero] Parâmetro 'valor' não é numérico (recebido: '#{valor}')" unless int?(valor) if valor <= 0 'Zero' # raise "[Exceção em Extenso.numero] Parâmetro 'valor' igual a ou menor que zero (recebido: '#{valor}')" elsif valor > VALOR_MAXIMO raise "[Exceção em Extenso::numero] Parâmetro ''valor'' deve ser um inteiro entre 1 e #{VALOR_MAXIMO} (recebido: '#{valor}')" elsif genero != GENERO_MASC && genero != GENERO_FEM raise "Exceção em Extenso: valor incorreto para o parâmetro 'genero' (recebido: '#{genero}')" # ------------------------------------------------ elsif valor >= 1 && valor <= 9 UNIDADES_ORDINAL[genero][valor] elsif valor >= 10 && valor <= 99 dezena = valor - (valor % 10) resto = valor - dezena ret = "#{DEZENAS_ORDINAL[genero][dezena]} " ret += ordinal(resto, genero) if resto.positive? ret.rstrip elsif valor >= 100 && valor <= 999 centena = valor - (valor % 100) resto = valor - centena ret = "#{CENTENAS_ORDINAL[genero][centena]} " ret += ordinal(resto, genero) if resto.positive? ret.rstrip elsif valor == 1000 MILHAR_ORDINAL[genero][valor] end end # Gera o valor em formato de Real # # Exemplo: # Extenso.real_formatado(10) - R$ 10,00 # Extenso.real_formatado(1.55) - R$ 1,55 # # @params[Object] def self.real_formatado(valor) float_valor = format('%#0.02f', valor) float_valor = float_valor.chars.reverse.insert(6, '.').reverse.join if float_valor.chars.count >= 7 float_valor = float_valor.chars.reverse.insert(10, '.').reverse.join if float_valor.chars.count >= 11 float_valor = float_valor.chars.reverse float_valor[2] = ',' "R$ #{float_valor.reverse.join}" end end