spec/calculator_spec.rb in dentaku-3.3.4 vs spec/calculator_spec.rb in dentaku-3.4.0

- old
+ new

@@ -145,10 +145,11 @@ end it 'stores nested hashes' do calculator.store(a: {basket: {of: 'apples'}}, b: 2) expect(calculator.evaluate!('a.basket.of')).to eq('apples') + expect(calculator.evaluate!('a.basket')).to eq(of: 'apples') expect(calculator.evaluate!('b')).to eq(2) end it 'stores arrays' do calculator.store(a: [1, 2, 3]) @@ -194,10 +195,19 @@ weekly_apple_budget: "apples * 7", pear: "1" )).to eq(pear: 1, weekly_apple_budget: 21, weekly_fruit_budget: 25) end + it "prefers variables over values in memory if they have no dependencies" do + expect(with_memory.solve!( + weekly_fruit_budget: "weekly_apple_budget + pear * 4", + weekly_apple_budget: "apples * 7", + pear: "1", + apples: "4" + )).to eq(apples: 4, pear: 1, weekly_apple_budget: 28, weekly_fruit_budget: 32) + end + it "preserves hash keys" do expect(calculator.solve!( 'meaning_of_life' => 'age + kids', 'age' => 40, 'kids' => 2 @@ -317,10 +327,14 @@ end expect { calculator.evaluate!('a + b') }.to raise_error do |error| expect(error.unbound_variables).to eq(['a', 'b']) end expect(calculator.evaluate(unbound)).to be_nil + end + + it 'accepts a block for custom handling of unbound variables' do + unbound = 'foo * 1.5' expect(calculator.evaluate(unbound) { :bar }).to eq(:bar) expect(calculator.evaluate(unbound) { |e| e }).to eq(unbound) end it 'fails to evaluate incomplete statements' do @@ -389,10 +403,11 @@ end it 'supports date arithmetic' do expect(calculator.evaluate!('2020-01-01 + 30').to_date).to eq(Time.local(2020, 1, 31).to_date) expect(calculator.evaluate!('2020-01-01 - 1').to_date).to eq(Time.local(2019, 12, 31).to_date) + expect(calculator.evaluate!('2020-01-01 - 2019-12-31')).to eq(1) expect(calculator.evaluate!('2020-01-01 + duration(1, day)').to_date).to eq(Time.local(2020, 1, 2).to_date) expect(calculator.evaluate!('2020-01-01 - duration(1, day)').to_date).to eq(Time.local(2019, 12, 31).to_date) expect(calculator.evaluate!('2020-01-01 + duration(30, days)').to_date).to eq(Time.local(2020, 1, 31).to_date) expect(calculator.evaluate!('2020-01-01 + duration(1, month)').to_date).to eq(Time.local(2020, 2, 1).to_date) expect(calculator.evaluate!('2020-01-01 - duration(1, month)').to_date).to eq(Time.local(2019, 12, 1).to_date) @@ -431,10 +446,88 @@ expect(calculator.evaluate('if (-1 = -1, -1, 5)')).to eq(-1) expect(calculator.evaluate('round(-1.23, 1)')).to eq(BigDecimal('-1.2')) expect(calculator.evaluate('NOT(some_boolean) AND -1 > 3', some_boolean: true)).to be_falsey end + describe "any" do + it "enumerates values and returns true if any evaluation is truthy" do + expect(calculator.evaluate!('any(xs, x, x > 3)', xs: [1, 2, 3, 4])).to be_truthy + expect(calculator.evaluate!('any(xs, x, x > 3)', xs: 3)).to be_falsy + expect(calculator.evaluate!('any({1,2,3,4}, x, x > 3)')).to be_truthy + expect(calculator.evaluate!('any({1,2,3,4}, x, x > 10)')).to be_falsy + expect(calculator.evaluate!('any(users, u, u.age > 33)', users: [ + {name: "Bob", age: 44}, + {name: "Jane", age: 27} + ])).to be_truthy + expect(calculator.evaluate!('any(users, u, u.age < 18)', users: [ + {name: "Bob", age: 44}, + {name: "Jane", age: 27} + ])).to be_falsy + end + end + + describe "all" do + it "enumerates values and returns true if all evaluations are truthy" do + expect(calculator.evaluate!('all(xs, x, x > 3)', xs: [1, 2, 3, 4])).to be_falsy + expect(calculator.evaluate!('any(xs, x, x > 2)', xs: 3)).to be_truthy + expect(calculator.evaluate!('all({1,2,3,4}, x, x > 0)')).to be_truthy + expect(calculator.evaluate!('all({1,2,3,4}, x, x > 10)')).to be_falsy + expect(calculator.evaluate!('all(users, u, u.age > 33)', users: [ + {name: "Bob", age: 44}, + {name: "Jane", age: 27} + ])).to be_falsy + expect(calculator.evaluate!('all(users, u, u.age < 50)', users: [ + {name: "Bob", age: 44}, + {name: "Jane", age: 27} + ])).to be_truthy + end + end + + describe "map" do + it "maps values" do + expect(calculator.evaluate!('map(xs, x, x * 2)', xs: [1, 2, 3, 4])).to eq([2, 4, 6, 8]) + expect(calculator.evaluate!('map({1,2,3,4}, x, x * 2)')).to eq([2, 4, 6, 8]) + expect(calculator.evaluate!('map(users, u, u.age)', users: [ + {name: "Bob", age: 44}, + {name: "Jane", age: 27} + ])).to eq([44, 27]) + expect(calculator.evaluate!('map(users, u, u.age)', users: [ + {"name" => "Bob", "age" => 44}, + {"name" => "Jane", "age" => 27} + ])).to eq([44, 27]) + expect(calculator.evaluate!('map(users, u, u.name)', users: [ + {name: "Bob", age: 44}, + {name: "Jane", age: 27} + ])).to eq(["Bob", "Jane"]) + expect(calculator.evaluate!('map(users, u, u.name)', users: [ + {"name" => "Bob", "age" => 44}, + {"name" => "Jane", "age" => 27} + ])).to eq(["Bob", "Jane"]) + end + end + + describe "pluck" do + it "plucks values from array of hashes" do + expect(calculator.evaluate!('pluck(users, age)', users: [ + {name: "Bob", age: 44}, + {name: "Jane", age: 27} + ])).to eq([44, 27]) + expect(calculator.evaluate!('pluck(users, age)', users: [ + {"name" => "Bob", "age" => 44}, + {"name" => "Jane", "age" => 27} + ])).to eq([44, 27]) + expect(calculator.evaluate!('pluck(users, name)', users: [ + {name: "Bob", age: 44}, + {name: "Jane", age: 27} + ])).to eq(["Bob", "Jane"]) + expect(calculator.evaluate!('pluck(users, name)', users: [ + {"name" => "Bob", "age" => 44}, + {"name" => "Jane", "age" => 27} + ])).to eq(["Bob", "Jane"]) + end + end + it 'evaluates functions with stored variables' do calculator.store("multi_color" => true, "number_of_sheets" => 5000, "sheets_per_minute_black" => 2000, "sheets_per_minute_color" => 1000) result = calculator.evaluate('number_of_sheets / if(multi_color, sheets_per_minute_color, sheets_per_minute_black)') expect(result).to eq(5) end @@ -601,10 +694,11 @@ describe 'math functions' do Math.methods(false).each do |method| it method do if Math.method(method).arity == 2 expect(calculator.evaluate("#{method}(x,y)", x: 1, y: '2')).to eq(Math.send(method, 1, 2)) + expect { calculator.evaluate!("#{method}(x)", x: 1) }.to raise_error(Dentaku::ParseError) else expect(calculator.evaluate("#{method}(1)")).to eq(Math.send(method, 1)) end end end @@ -700,8 +794,26 @@ it 'should allow optout of nested hash' do expect do without_nested_data.solve!('a.b.c') end.to raise_error(Dentaku::UnboundVariableError) + end + end + + describe 'identifier cache' do + it 'reduces call count by caching results of resolved identifiers' do + called = 0 + calculator.store_formula("A1", "B1+B1+B1") + calculator.store_formula("B1", "C1+C1+C1+C1") + calculator.store_formula("C1", "D1") + calculator.store("D1", proc { called += 1; 1 }) + + expect { + Dentaku.enable_identifier_cache! + }.to change { + called = 0 + calculator.evaluate("A1") + called + }.from(12).to(1) end end end