#!/usr/bin/ruby
# AUTHOR: Daniel "Trizen" Șuteu
# LICENSE: GPLv3
class new(diacritics = true,
invalid_number = nil,
negative_sign = 'minus',
decimal_point = 'virgulă',
thousands_separator = '',
infinity = 'infinit',
not_a_number = 'NaN') {
# This function removes the Romanian diacritics from a given text.
func _remove_diacritics(s) {
s.tr('ăâșțî','aasti');
}
# Numbers => text
static DIGITS = Hash();
DIGITS{@|0..19} = %w(
zero unu doi trei patru cinci șase șapte opt nouă zece
unsprezece
doisprezece
treisprezece
paisprezece
cincisprezece
șaisprezece
șaptesprezece
optsprezece
nouăsprezece
)...;
# Text => numbers
static WORDS = Hash()
WORDS{@|DIGITS.values.map {|v|_remove_diacritics(v)}} = DIGITS.keys.map{.to_num}...
WORDS{@|<o un doua sai>} = (1, 1, 2, 6)
# Colocvial
WORDS{@|<unspe doispe treispe paispe cinspe cinsprezece saispe saptespe saptuspe optspe nouaspe>} =
(11, 12, 13, 14, 15, 15, 16, 17, 17, 18, 19);
# This array contains number greater than 1000 and it's used to convert numbers into text
# See: https://ro.wikipedia.org/wiki/Sistem_zecimal#Denumiri_ale_numerelor
static BIGNUMS = (
[
[ 10**2, 'suta', 'sute', true],
[ 10**3, 'mie', 'mii', true],
[ 10**6, 'milion', 'milioane', false],
[ 10**9, 'miliard', 'miliarde', false],
[10**12, 'bilion', 'bilioane', false],
[10**15, 'biliard', 'biliarde', false],
[10**18, 'trilion', 'trilioane', false],
[10**21, 'triliard', 'triliarde', false],
[10**24, 'cvadrilion', 'cvadrilioane', false],
[10**27, 'cvadriliard', 'cvadriliarde', false],
[Inf, 'inifinit', 'infinit', false],
].map { |v|
var h = Hash()
h{@|<num sg pl fem>} = v...
h
}
);
# This hash is a reversed version of the above array and it's used to convert text into numbers
static BIGWORDS = Hash()
BIGNUMS.each { |x|
BIGWORDS{x{:sg},x{:pl}} = (x{:num}, x{:num});
}
# Change 'suta' to 'sută'
BIGNUMS[0]{:sg} = 'sută';
# This functions removes irrelevant characters from a text
func _normalize_text(s) {
# Lowercase and remove the diacritics
var text = _remove_diacritics(s.lc);
# Replace irrelevant characters with a space
return text.tr('a-z', ' ', 'c');
}
# This function adds together a list of numbers
func _add_numbers(nums) {
var num = 0;
while (nums.len) {
var i = nums.shift;
# When the current number is lower than the next number
if (nums.len && (i < nums[0])) {
var n = nums.shift;
# Factor (e.g.: 400 -> 4)
var f = idiv(i, 10**i.ilog10);
# When the next number is not really next to the current number
# e.g.: $i == 400 and $n == 5000 # will produce 405_000, not 45_000
if ((var mod = (n.len % 3)) != 0) {
f *= 10**(3 - mod);
}
# Join the numbers and continue
num += (10**n.ilog10 * f + n);
next;
}
num += i;
}
return num;
}
# This function converts a Romanian
# text-number into a mathematical number.
method ro_to_number(text) {
# When text is not a string
text.is_a(String) || return();
# If a thousand separator is defined, remove it from text
if (self.thousands_separator != '' && (self.thousands_separator.len > 1)) {
text.gsub!(self.thousands_separator, ' ');
}
# Split the text into words
var words = _normalize_text(text).words;
var dec_point = _normalize_text(self.decimal_point);
var neg_sign = _normalize_text(self.negative_sign);
var nums = []; # numbers
var decs = []; # decimal numbers
var neg = false; # bool -- true when the number is negative
var adec = false; # bool -- true after the decimal point
var amount = 0; # int -- current number
var factor = 1; # int -- multiplication factor
if (words.len) {
# Check for negative numbers
if (words[0] == neg_sign) {
neg = true;
words.shift;
}
# Check for infinity and NaN
if (words.len == 1) {
# Infinity
var inf = _normalize_text(self.infinity);
if (words[0] == inf) {
return(neg ? -Inf : Inf);
}
# Not a number
var nan = _normalize_text(self.not_a_number);
if (words[0] == nan) {
return NaN
}
}
}
# Iterate over the @words
while ( {words.len && (
# It's a small number (lower than 100)
(factor = (WORDS.exists(words[0]) ? 1 : (words[0].ends_with('zeci') ? do { words[0].sub!(/zeci\z/); 10 } : 0));
factor > 0 && (amount = words.shift);
factor > 0) || (
# It's a big number (e.g.: milion)
(
words.len && BIGWORDS.has_key(words[0]) && do {
factor = BIGWORDS{words.shift};
factor > 0
}
) ||
# Ignore invalid words
(
words.shift;
__BLOCK__.run;
)
))}.run
) {
# Take and multiply the current number
var num = (WORDS.has_key(amount) ? (WORDS{amount} * factor) : next); # skip invalid words
# Check for some word-joining tokens
if (words.len) {
if (words[0] == 'si') { # e.g.: patruzeci si doi
words.shift;
num += WORDS{words.shift};
}
if (words.len) {
{
if (words[0] == 'de') { # e.g.: o suta de mii
words.shift;
}
if (BIGWORDS.has_key(words[0])) {
num *= BIGWORDS{words.shift};
}
if (words.len && (words[0] == 'de')) {
__BLOCK__.run;
}
}.run;
}
}
# If we are after the decimal point, store the
# numbers in @decs, otherwise store them in @nums.
[nums,decs][adec].push(num);
# Check for the decimal point
if (words.len && (words[0] == dec_point)) {
adec = true;
words.shift;
}
}
# Return undef when no number has been converted
nums.len || return();
# Add all the numbers together (if any)
var num = _add_numbers(nums).to_s;
# If the number contains decimals,
# add them at the end of the number
if (decs.len) {
# Special case -- check for leading zeros
var zeros = '';
while (decs.len && (decs[0] == 0)) {
zeros += decs.shift.to_s;
}
num += ('.' + zeros + _add_numbers(decs).to_s);
}
# Return the number
return(neg ? num.to_num.neg : num.to_num);
}
method _number_to_ro(number) {
var words = [];
if (DIGITS.has_key(number)) {
words.append(DIGITS{number});
}
elsif (number.to_num! -> is_nan) {
return [self.not_a_number];
}
elsif (number.is_negative) {
words.append(self.negative_sign);
words.append(__METHOD__(self, number.abs)...);
}
elsif (!(number.is_int)) {
words.append(__METHOD__(self, number.int)...);
words.append(self.decimal_point);
number -= number.int;
while (number != number.int) {
number *= 10 < 1 && (words.append(DIGITS{0}));
}
words.append(__METHOD__(self, number.int)...);
}
elsif (number >= BIGNUMS[0]{:num}) {
for i in range(BIGNUMS.end) {
var j = BIGNUMS.end-i;
if (number >= BIGNUMS[j-1]{:num} && (number < BIGNUMS[j]{:num})) {
var cat = (number / BIGNUMS[j-1]{:num} -> int);
number -= (BIGNUMS[j-1]{:num} * (number / BIGNUMS[j-1]{:num} -> int));
var of = (cat <= 2 ? [] : do {
var w = (
DIGITS.has_key(cat)
? [DIGITS{cat}]
: (__METHOD__(self, cat) + ['de'])
);
w.len > 2 && (w[-2] == DIGITS{2} && (w[-2] = 'două'));
w;
});
if (cat >= 100 && (cat < 1000)) {
var rest = (cat - (100 * (cat / 100 -> int)));
if (of.len != 0 && (rest != 0 && (DIGITS.has_key(rest)))) {
of.pop;
}
}
words += (
cat == 1 ? [BIGNUMS[j-1]{:fem} ? 'o' : 'un', BIGNUMS[j-1]{:sg}]
: (cat == 2 ? ['două', BIGNUMS[j-1]{:pl}]
: (of + [BIGNUMS[j-1]{:pl}]));
);
if (number > 0) {
if (BIGNUMS[j]{:num} > 1000) {
words[-1] += self.thousands_separator
}
words.append(__METHOD__(self, number)...)
}
break
}
}
}
elsif (number > 19 && (number < 100)) {
var cat = (number / 10 -> int);
words.append(
(
cat == 2 ? 'două'
: (
cat == 6 ? ('șai')
: (DIGITS{cat})
)
) + 'zeci'
);
if (number % 10 != 0) {
words.append('și', DIGITS{number % 10 -> int});
}
}
elsif (number.is_inf) {
return [self.infinity];
}
else {
return([self.invalid_number]);
}
return(words);
}
method number_to_ro(num) {
var word = self._number_to_ro(num).join(" ");
if (!self.diacritics) {
word = _remove_diacritics(word);
}
return word;
}
}