Last active
March 25, 2021 03:24
-
-
Save tompng/efed9b2c7d77ab917c2a263f77272b4f to your computer and use it in GitHub Desktop.
strong zero
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Parser | |
TOKENS = {} | |
TOKEN_INFO = { | |
_: '_', | |
a: 'a', | |
# TODO: an undecillion, an octillion, an octodecillion | |
and: 'and', | |
minus: 'minus', | |
zero: 'zero', | |
one_nine: [nil] + %w[one two three four five six seven eight nine], | |
teen: %w[ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen], | |
ty: { | |
twenty: 20, | |
thirty: 30, | |
forty: 40, | |
fourty: 40, | |
fifty: 50, | |
sixty: 60, | |
seventy: 70, | |
eighty: 80, | |
ninety: 90 | |
}, | |
point: 'point', | |
hundred: 'hundred', | |
thousand: 'thousand', | |
llion: [nil, nil] + %w[ | |
million billion trillion quadrillion quintillion | |
sextillion septillion octillion nonillion decillion | |
undecillion duodecillion tredecillion quattuordecillion quindecillion | |
sexdecillion septendecillion octodecillion novemdecillion vigintillion | |
], | |
half_quarter: { | |
half: [2, false], | |
halfs: [2, true], | |
quarter: [4, false], | |
quarters: [4, true] | |
}, | |
ext_th: { | |
first: [1, :th], | |
second: [2, :th], | |
third: [3, :th], | |
thirds: [3, :ths], | |
fifth: [5, :th], | |
fifths: [5, :ths], | |
eighth: [8, :th], | |
eighths: [8, :th], | |
ninth: [9, :th], | |
ninths: [9, :ths], | |
twelvth: [12, :th], | |
twelvths: [12, :ths], | |
twentieth: [20, :th], | |
twentieths: [20, :ths], | |
thirtieth: [30, :th], | |
thirtieths: [30, :ths], | |
fourtieth: [40, :th], | |
fourtieths: [40, :ths], | |
fiftieth: [50, :th], | |
fiftieths: [50, :ths], | |
sixtieth: [60, :th], | |
sixtieths: [60, :ths], | |
seventieth: [70, :th], | |
seventieths: [70, :ths], | |
eightieth: [80, :th], | |
eightieths: [80, :ths], | |
ninetieth: [90, :th], | |
ninetieths: [90, :ths] | |
}, | |
th: 'th', | |
ths: 'ths', | |
last: 'last', | |
over: 'over', | |
:'?' => '?' | |
} | |
TOKEN_INFO.each do |key, value| | |
if value.is_a? Hash | |
value.each {|v, data| TOKENS[v.to_s] = [key, data] } | |
elsif value.is_a? String | |
TOKENS[value.to_s] = [key] | |
else | |
value.each_with_index {|v, i| TOKENS[v.to_s] = [key, i] if v } | |
end | |
end | |
TOKEN_IDS = TOKEN_INFO.keys.each_with_index.to_h do |type, index| | |
[type.to_sym, ('a'.ord + index).chr] | |
end | |
TOKEN_REGEXP = Regexp.union TOKENS.keys.sort.reverse | |
def self.tokenize(s) | |
tokens = s.scan TOKEN_REGEXP | |
return if tokens.sum(&:size) != s.size | |
return if tokens.first == '_' || tokens.last == '_' || tokens.each_cons(2).any? { _1 == '_' && _2 == '_' } | |
tokens -= ['_'] | |
tokens.map(&TOKENS) | |
end | |
def self.pattern | |
return @pattern if @pattern | |
keta12 = '(teen | ty one_nine? | one_nine)' | |
keta13 = "(one_nine? hundred (and? #{keta12})? | #{keta12})" | |
# TODO: nineteen_o_four ninety_ninety_nine | |
keta15 = "(#{keta13}? thousand and? #{keta13}? | #{keta12}? hundred (and? #{keta12})? | #{keta12})" | |
hnum = "((a | minus? #{keta12}?) hundred and? #{keta12}?)" | |
tnum = "((a | minus? #{keta13}?) thousand and? #{keta13}?)" | |
lnum = "((a | minus? #{keta13}?) llion (and? #{keta13}? llion)* and? #{keta15}?)" | |
nonzero = "(#{lnum} | #{tnum} | #{hnum} | minus? #{keta15})" | |
num = "(#{nonzero} | minus? zero)" | |
nth = "#{num} th (last)?" | |
rational_hq = "(a | #{num} | minus |) half_quarter" | |
rational_th = "(a | minus? (zero | one_nine)) #{nonzero} (th | ths)" | |
rational_over = "(#{num} over #{nonzero})" | |
number = "#{num} (point (zero | one_nine)+)? | point (zero | one_nine)+" | |
patterns = [nth, number, rational_hq, rational_th, rational_over].map do |s| | |
"(#{s.gsub('(', '(?:')})" | |
end | |
joined_pattern = patterns.join('|').gsub(/[a-z_]+/) { TOKEN_IDS[_1.to_sym] }.delete(' ') | |
@pattern = Regexp.new "\\A(?:#{joined_pattern})\\z" | |
end | |
def self.parse(s) | |
tokens = tokenize s | |
parse_tokens tokens if tokens | |
end | |
def self.parse_tokens(tokens) | |
tokens.pop if (question = tokens.last&.first == :'?') | |
tokens.pop if (last = tokens.last&.first == :last) | |
if tokens.last&.first == :ext_th | |
_t, (num, th_type) = tokens.pop | |
if num >= 20 | |
tokens.push [:ty, num / 10], [th_type] | |
elsif num >= 10 | |
tokens.push [:teen, num % 10], [th_type] | |
else | |
tokens.push [:one_nine, num], [th_type] | |
end | |
elsif [:th, :ths].include? tokens.last&.first | |
type, data = tokens[-2] | |
return if type == :ty | |
return if type == :one_nine && ![4, 6, 7].include?(data) | |
end | |
structure = tokens.map { TOKEN_IDS[_1.first] }.join | |
matched = structure.match pattern | |
return unless matched | |
return question ? nil : parse_nth(tokens, last) if matched[1] | |
return if last | |
value = begin | |
if matched[2] | |
parse_number(tokens) | |
else | |
if matched[3] | |
parse_rational_hq(tokens) | |
elsif matched[4] | |
parse_rational_th(tokens) | |
else | |
parse_rational_over(tokens) | |
end | |
end | |
end | |
{ type: :number, value: value, question: question } if value | |
end | |
def self.parse_nth(tokens, reverse) | |
tokens.pop | |
value = parse_number(tokens) | |
{ type: :nth, value: value, reverse: reverse } if value | |
end | |
def self.parse_number(tokens) | |
tokens.shift if (minus = tokens.first&.first == :minus) | |
sgn = minus ? -1 : +1 | |
point_index = tokens.find_index { _1[0] == :point } | |
if point_index | |
float = parse_points tokens[point_index + 1..] | |
tokens = tokens[0...point_index] | |
return sgn * float if tokens.size.zero? | |
end | |
committed = 0 | |
uncommitted = nil | |
thousands = nil | |
tokens.each do |type, data| | |
case type | |
when :zero | |
uncommitted = 0 | |
when :a | |
uncommitted = 1 | |
when :one_nine | |
uncommitted = (uncommitted || 0) + data | |
when :teen | |
uncommitted = (uncommitted || 0) + 10 + data | |
when :ty | |
uncommitted = (uncommitted || 0) + data | |
when :hundred | |
if uncommitted | |
uncommitted *= 100 | |
else | |
uncommitted = 100 | |
end | |
when :thousand, :llion | |
base = 1000 ** (type == :thousand ? 1 : data) | |
return if thousands && thousands <= base | |
committed += (uncommitted || 1) * base | |
thousands = base | |
uncommitted = nil | |
end | |
end | |
return sgn * (committed + (uncommitted || 0) + (float || 0)) | |
end | |
def self.parse_points(tokens) | |
value = 0 | |
tokens.reverse_each do | |
value = (value + (_1 == :zero ? 0 : _2)).fdiv 10 | |
end | |
value | |
end | |
def self.parse_rational_hq(tokens) | |
tokens.shift if (minus = tokens.first&.first == :minus) | |
sgn = minus ? -1 : +1 | |
denominator, multiple = tokens.pop.last | |
tokens.shift if tokens.first&.first == :a | |
num = sgn * (tokens.empty? ? 1 : parse_number(tokens)) | |
return unless num | |
return if num.abs == 1 && multiple | |
num.quo denominator | |
end | |
def self.parse_rational_th(tokens) | |
tokens.shift if (minus = tokens.first&.first == :minus) | |
sgn = minus ? -1 : +1 | |
multiple = tokens.pop.first == :ths | |
type, data = tokens.shift | |
numerator = sgn * (type == :a ? 1 : type == :zero ? 0 : data) | |
return if numerator.abs == 1 && multiple | |
denominator = parse_number tokens | |
return numerator.quo denominator | |
end | |
def self.parse_rational_over(tokens) | |
index = tokens.find_index { _1[0] == :over } | |
a = parse_number tokens[0...index] | |
b = parse_number tokens[index+1..] | |
a.quo b if a && b | |
end | |
def self.strict_parse(s, cond) | |
parsed = parse s | |
parsed if parsed && cond.all? { parsed[_1] == _2 } | |
end | |
def self.parse_dot_points(s) | |
tokens = tokenize s | |
return unless tokens&.all? { |type, _| type == :zero || type == :one_nine } | |
parse_points tokens | |
end | |
end | |
module NumberEq | |
def method_missing(name, *args) | |
if args.empty? | |
parsed = Parser.strict_parse name.to_s, { type: :number, question: true } | |
return self == parsed[:value] if parsed | |
end | |
super | |
end | |
def respond_to_missing?(name, _) | |
return true if Parser.strict_parse name.to_s, { type: :number, question: true } | |
super | |
end | |
end | |
module SizeEq | |
def method_missing(name, *args) | |
if args.empty? | |
parsed = Parser.strict_parse name.to_s, { type: :number, question: true } | |
return size == parsed[:value] if parsed && parsed[:value].is_a?(Integer) | |
end | |
super | |
end | |
def respond_to_missing?(name, _) | |
parsed = Parser.strict_parse name.to_s, { type: :number, question: true } | |
return true if parsed && parsed[:value].is_a?(Integer) | |
super | |
end | |
end | |
module Nth | |
def method_missing(name, *args) | |
if args.empty? | |
if (parsed = Parser.strict_parse name.to_s, { type: :nth }) | |
value = parsed[:value] * (parsed[:reverse] ? -1 : 1) | |
index = value >= 1 ? value - 1 : value | |
return index == 0 ? first : index == -1 ? last : to_a[index] | |
end | |
end | |
super | |
end | |
def respond_to_missing?(name, _) | |
return true if Parser.strict_parse name.to_s, { type: :nth } | |
super | |
end | |
end | |
module DotPoints | |
def method_missing(name, *args) | |
if args.empty? | |
v = Parser.parse_dot_points name.to_s | |
return self + (self < 0 ? -v : v) if v | |
end | |
super | |
end | |
def respond_to_missing?(name, _) | |
return true if Parser.parse_dot_points name.to_s | |
super | |
end | |
end | |
module MagicalNumber | |
def method_missing(name, *) | |
super | |
rescue NoMethodError | |
raise | |
rescue NameError | |
parsed = Parser.strict_parse name.to_s, { type: :number, question: false } | |
parsed ? parsed[:value] : raise | |
end | |
def respond_to_missing?(name, include_private) | |
if include_private | |
parsed = Parser.strict_parse name.to_s, { type: :number, question: false } | |
return true if parsed | |
end | |
super | |
end | |
end | |
Numeric.include NumberEq | |
Enumerable.include SizeEq | |
Enumerable.include Nth | |
Integer.include DotPoints | |
Kernel.prepend MagicalNumber | |
# TODO: refinements | |
binding.irb | |
__END__ | |
10020304.ten_million_twenty_thousand_three_hundred_and_four? #=> true | |
3.14.three_point_one_four? #=> true | |
minus_three.one_four_one_five_nine #=> -3.14159 | |
(1..1010).ten_hundred_ten? #=> true | |
(1..10).second_last #=> 9 | |
(1..10).minus_third #=> 8 | |
(1..1000).three_hundred_forty_fifth #=> 345 | |
(-123/53r).minus_one_hundred_twenty_three_over_fifty_three? #=> true | |
(3/4r).three_quarters? #=> true | |
(-2/133r).minus_two_one_hundred_thirty_third? #=> true | |
a_thousand_and_nine_point_zero #=> 1009.0 | |
minus_three_fifths #=> (-3/5) | |
onehundredandone #=> 101 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment