Class: Synthra::Types::Formula::SafeArithmetic
- Inherits:
-
Object
- Object
- Synthra::Types::Formula::SafeArithmetic
- Defined in:
- lib/synthra/types/formula.rb
Overview
Sandboxed arithmetic evaluator: numbers, + - * / % **, parentheses, and the
whitelisted math functions below. No eval, so it is safe for tenant-supplied
expressions (the RCE foot-gun is removed). Returns nil for anything outside this
grammar — the caller treats nil as 0. Recursive-descent; state is per-instance so
it is safe to reuse the type across generations.
Constant Summary collapse
- FUNCTIONS =
{ "min" => ->(a) { a.min }, "max" => ->(a) { a.max }, "abs" => ->(a) { a.first.abs }, "sqrt" => ->(a) { Math.sqrt(a.first) }, "floor" => ->(a) { a.first.floor }, "ceil" => ->(a) { a.first.ceil }, "round" => ->(a) { a.length > 1 ? a[0].round(a[1].to_i) : a[0].round }, "pow" => ->(a) { a[0]**a[1] } }.freeze
Class Method Summary collapse
Instance Method Summary collapse
- #apply_mul(op, lhs, rhs) ⇒ Object private
-
#initialize(expression) ⇒ SafeArithmetic
constructor
A new instance of SafeArithmetic.
- #parse_add ⇒ Object private
- #parse_function(name) ⇒ Object private
- #parse_group ⇒ Object private
- #parse_mul ⇒ Object private
- #parse_pow ⇒ Object private
- #parse_primary ⇒ Object private
- #parse_unary ⇒ Object private
- #peek ⇒ Object private
- #result ⇒ Object
- #take ⇒ Object private
-
#tokenize(expression) ⇒ Object
private
Tokenize and reject (return []) if any character isn't part of the grammar — e.g.
Constructor Details
#initialize(expression) ⇒ SafeArithmetic
Returns a new instance of SafeArithmetic.
72 73 74 75 |
# File 'lib/synthra/types/formula.rb', line 72 def initialize(expression) @tokens = tokenize(expression) @pos = 0 end |
Class Method Details
.eval(expression) ⇒ Object
66 67 68 69 70 |
# File 'lib/synthra/types/formula.rb', line 66 def self.eval(expression) new(expression).result rescue StandardError nil end |
Instance Method Details
#apply_mul(op, lhs, rhs) ⇒ Object (private)
121 122 123 124 125 126 127 |
# File 'lib/synthra/types/formula.rb', line 121 def apply_mul(op, lhs, rhs) case op when "*" then lhs * rhs when "/" then lhs.to_f / rhs else lhs % rhs end end |
#parse_add ⇒ Object (private)
103 104 105 106 107 108 109 |
# File 'lib/synthra/types/formula.rb', line 103 def parse_add value = parse_mul while ["+", "-"].include?(peek) value = take == "+" ? value + parse_mul : value - parse_mul end value end |
#parse_function(name) ⇒ Object (private)
168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/synthra/types/formula.rb', line 168 def parse_function(name) fn = FUNCTIONS[name] raise "unknown function #{name}" unless fn take # name raise "expected (" unless take == "(" args = [parse_add] args << parse_add while peek == "," && take raise "expected )" unless take == ")" fn.call(args) end |
#parse_group ⇒ Object (private)
160 161 162 163 164 165 166 |
# File 'lib/synthra/types/formula.rb', line 160 def parse_group take # "(" value = parse_add raise "missing )" unless take == ")" value end |
#parse_mul ⇒ Object (private)
111 112 113 114 115 116 117 118 119 |
# File 'lib/synthra/types/formula.rb', line 111 def parse_mul value = parse_pow while ["*", "/", "%"].include?(peek) op = take rhs = parse_pow value = apply_mul(op, value, rhs) end value end |
#parse_pow ⇒ Object (private)
129 130 131 132 133 134 135 |
# File 'lib/synthra/types/formula.rb', line 129 def parse_pow base = parse_unary return base unless peek == "**" take base**parse_pow # right-associative end |
#parse_primary ⇒ Object (private)
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/synthra/types/formula.rb', line 144 def parse_primary tok = peek raise "unexpected end" if tok.nil? if tok == "(" parse_group elsif tok.match?(/\A\d/) take tok.include?(".") ? tok.to_f : tok.to_i elsif tok.match?(/\A[A-Za-z_]/) parse_function(tok) else raise "unexpected token #{tok}" end end |
#parse_unary ⇒ Object (private)
137 138 139 140 141 142 |
# File 'lib/synthra/types/formula.rb', line 137 def parse_unary return parse_primary unless peek == "-" take -parse_unary end |
#peek ⇒ Object (private)
93 94 95 |
# File 'lib/synthra/types/formula.rb', line 93 def peek @tokens[@pos] end |
#result ⇒ Object
77 78 79 80 81 82 |
# File 'lib/synthra/types/formula.rb', line 77 def result return nil if @tokens.empty? value = parse_add @pos == @tokens.length ? value : nil end |
#take ⇒ Object (private)
97 98 99 100 101 |
# File 'lib/synthra/types/formula.rb', line 97 def take tok = @tokens[@pos] @pos += 1 tok end |
#tokenize(expression) ⇒ Object (private)
Tokenize and reject (return []) if any character isn't part of the grammar — e.g. quotes, method calls, or leftover identifiers from a non-arithmetic field.
88 89 90 91 |
# File 'lib/synthra/types/formula.rb', line 88 def tokenize(expression) tokens = expression.to_s.scan(%r{\d+\.\d+|\d+|[A-Za-z_]\w*|\*\*|[-+*/%(),]}) tokens.join == expression.to_s.gsub(/\s+/, "") ? tokens : [] end |