Skip to content

Instantly share code, notes, and snippets.

@tompng
Last active March 25, 2021 03:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tompng/efed9b2c7d77ab917c2a263f77272b4f to your computer and use it in GitHub Desktop.
Save tompng/efed9b2c7d77ab917c2a263f77272b4f to your computer and use it in GitHub Desktop.
strong zero
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