dentaku 4.0.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/lib/dentaku/ast/combinators.rb +22 -4
- data/lib/dentaku/ast/functions/duration.rb +3 -3
- data/lib/dentaku/ast/functions/xor.rb +6 -6
- data/lib/dentaku/bulk_expression_solver.rb +9 -6
- data/lib/dentaku/numeric_parser.rb +7 -6
- data/lib/dentaku/token_scanner.rb +1 -7
- data/lib/dentaku/tokenizer.rb +0 -1
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/xor_spec.rb +16 -3
- data/spec/bulk_expression_solver_spec.rb +22 -0
- data/spec/calculator_spec.rb +41 -0
- data/spec/case_sensitivity_spec.rb +54 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 75997c319d555caca8df23e6b1e1995f5a99b1fbd041f25d1e876ac40b487b49
|
|
4
|
+
data.tar.gz: ee629e3909f7b5040d9f39c1cad871f3d7599be8a76d6b2753652bcdbc9aefbc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f7342cd68c9b1edbd2c992b01784a3b66caa6f13f0cc698a26447883a01eb6ffde1c6262790d9810ded42d5bdd6ac601ab2c7585e4c6d5f5467964cb6fe92ce
|
|
7
|
+
data.tar.gz: 8b1c85908541da4fe4896b3fcb90741fe8ea907c29fc162fcb64eca9ae60bd043ecdece5fe428490c0452a6f1072174a52b090ab575f50d117cbe3f976ad0265
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## [v4.1.0] 2026-10-14
|
|
4
|
+
- `NumericParser` compiles its regular expressions once instead of on every
|
|
5
|
+
call. `Comparator#cast` runs one per string operand of every comparison,
|
|
6
|
+
which made the regex compilation the single largest cost of evaluating a
|
|
7
|
+
typical condition: `country = 'US'` evaluated 30k times drops from 0.47s
|
|
8
|
+
to 0.07s
|
|
9
|
+
- fix dependency analysis of `AND` / `OR` chains taking exponential time
|
|
10
|
+
when a short-circuit probe raises (e.g. a zero denominator), a 4.0.0
|
|
11
|
+
regression on the normal `evaluate!` path (#355)
|
|
12
|
+
- `duration()` only accepts exact unit names (`day`, `days`, `month`,
|
|
13
|
+
`months`, `year`, `years`); units merely containing one, such as
|
|
14
|
+
`dayofmonth`, are now rejected
|
|
15
|
+
- `case_sensitive` is no longer shared between calculator instances. The
|
|
16
|
+
tokenizer read the setting from a class-level variable that every
|
|
17
|
+
calculator overwrote, so calculators with different settings running in
|
|
18
|
+
parallel threads could silently fold identifiers to the wrong case and
|
|
19
|
+
return wrong results. Casing is now applied only by the calculator's own
|
|
20
|
+
parser; `Tokenizer#tokenize` keeps identifiers' source casing and
|
|
21
|
+
`TokenScanner.case_sensitive` is removed
|
|
22
|
+
- key the bulk-solver dependency-order cache by the resolved dependency graph,
|
|
23
|
+
so changing formulas or stored context cannot reuse an incompatible order
|
|
24
|
+
from another solve with the same variable names.
|
|
25
|
+
- **behavior change:** `XOR` with three or more arguments is now the
|
|
26
|
+
exclusive OR of all of them (true when an odd number are true), matching
|
|
27
|
+
Excel and `XOR(XOR(a, b), c)`. It previously returned true only when
|
|
28
|
+
exactly one argument was true, so `XOR(true, true, true)` changes from
|
|
29
|
+
`false` to `true`. Results with one or two arguments are unchanged (#349)
|
|
30
|
+
|
|
3
31
|
## [v4.0.2] 2026-08-02
|
|
4
32
|
- the 4.0.1 guard against arithmetic that rejects a well-typed operand only
|
|
5
33
|
worked on Ruby 3.4+. Ruby 3.2 and 3.3 do not raise for an oversized
|
|
@@ -21,17 +21,35 @@ module Dentaku
|
|
|
21
21
|
:logical
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
# A short-circuit probe that raises tells us nothing about which operands are
|
|
25
|
+
# referenced, so fall back to the union of both sides. It has to be the union we
|
|
26
|
+
# already computed rather than `super`: `super` re-derives it by walking both
|
|
27
|
+
# subtrees again, and because the probe raises at every level of a chain, each
|
|
28
|
+
# level would walk its subtree twice -- making analysis exponential in the
|
|
29
|
+
# length of the chain.
|
|
24
30
|
def dependencies(context = {})
|
|
25
31
|
return super if static_mode?(context)
|
|
26
32
|
|
|
27
33
|
left_deps = left.dependencies(context)
|
|
28
34
|
right_deps = right.dependencies(context)
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
|
|
36
|
+
if left_deps.empty? && left.pure?
|
|
37
|
+
begin
|
|
38
|
+
return [] if decisive?(left.value(context))
|
|
39
|
+
rescue Dentaku::Error
|
|
40
|
+
return (left_deps + right_deps).uniq
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if right_deps.empty? && right.pure?
|
|
45
|
+
begin
|
|
46
|
+
return [] if decisive?(right.value(context))
|
|
47
|
+
rescue Dentaku::Error
|
|
48
|
+
return (left_deps + right_deps).uniq
|
|
49
|
+
end
|
|
50
|
+
end
|
|
31
51
|
|
|
32
52
|
(left_deps + right_deps).uniq
|
|
33
|
-
rescue Dentaku::Error
|
|
34
|
-
super
|
|
35
53
|
end
|
|
36
54
|
|
|
37
55
|
def value(context = {})
|
|
@@ -21,9 +21,9 @@ module Dentaku
|
|
|
21
21
|
|
|
22
22
|
def validate_unit(unit)
|
|
23
23
|
case unit.downcase
|
|
24
|
-
when /
|
|
25
|
-
when /
|
|
26
|
-
when /
|
|
24
|
+
when /\Ayears?\z/ then :year
|
|
25
|
+
when /\Amonths?\z/ then :month
|
|
26
|
+
when /\Adays?\z/ then :day
|
|
27
27
|
else
|
|
28
28
|
raise Dentaku::ArgumentError.for(:incompatible_type, actual: unit, expected: Duration),
|
|
29
29
|
"'#{unit || unit.class}' is not a valid duration unit"
|
|
@@ -20,14 +20,14 @@ module Dentaku
|
|
|
20
20
|
)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
# exclusive OR of all arguments: true when an odd number are true, so
|
|
24
|
+
# XOR(a, b, c) == XOR(XOR(a, b), c)
|
|
25
|
+
true_arg_count = @args.count do |arg|
|
|
25
26
|
case arg.value(context)
|
|
26
27
|
when TrueClass
|
|
27
|
-
|
|
28
|
-
break if true_arg_count > 1
|
|
28
|
+
true
|
|
29
29
|
when FalseClass, nil
|
|
30
|
-
|
|
30
|
+
false
|
|
31
31
|
else
|
|
32
32
|
raise Dentaku::ArgumentError.for(
|
|
33
33
|
:incompatible_type,
|
|
@@ -35,7 +35,7 @@ module Dentaku
|
|
|
35
35
|
)
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
|
-
true_arg_count
|
|
38
|
+
true_arg_count.odd?
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
end
|
|
@@ -153,12 +153,15 @@ module Dentaku
|
|
|
153
153
|
end
|
|
154
154
|
|
|
155
155
|
def variables_in_resolve_order
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
@ordered_deps ||= begin
|
|
157
|
+
dependency_graph = dependencies
|
|
158
|
+
cache_key = [!!calculator.case_sensitive, dependency_graph]
|
|
159
|
+
self.class.dependency_cache.fetch(cache_key) {
|
|
160
|
+
DependencyResolver.find_resolve_order(dependency_graph, calculator.case_sensitive).tap do |d|
|
|
161
|
+
self.class.dependency_cache[cache_key] = d if calculator.cache_dependency_order?
|
|
162
|
+
end
|
|
163
|
+
}
|
|
164
|
+
end
|
|
162
165
|
end
|
|
163
166
|
end
|
|
164
167
|
end
|
|
@@ -5,20 +5,21 @@ module Dentaku
|
|
|
5
5
|
HEXADECIMAL_PATTERN = '(0x[0-9a-f]+)\b'.freeze
|
|
6
6
|
# e.g: [0x1A3F]
|
|
7
7
|
|
|
8
|
+
NUMERIC_OR_HEXADECIMAL_REGEX = %r{\A(-?(#{ NUMERIC_PATTERN }|#{ HEXADECIMAL_PATTERN }))\z}i
|
|
9
|
+
NUMERIC_REGEX = %r{\A(#{ NUMERIC_PATTERN })\z}i
|
|
10
|
+
HEXADECIMAL_REGEX = %r{\A(#{ HEXADECIMAL_PATTERN })\z}i
|
|
11
|
+
|
|
8
12
|
class << self
|
|
9
13
|
def match(string)
|
|
10
|
-
|
|
11
|
-
string.match(regex)
|
|
14
|
+
string.match(NUMERIC_OR_HEXADECIMAL_REGEX)
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
def match_numeric(string)
|
|
15
|
-
|
|
16
|
-
string.match(regex)
|
|
18
|
+
string.match(NUMERIC_REGEX)
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
def match_hexadecimal(string)
|
|
20
|
-
|
|
21
|
-
string.match(regex)
|
|
22
|
+
string.match(HEXADECIMAL_REGEX)
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def parse_numeric_string(raw)
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
require 'bigdecimal'
|
|
2
2
|
require 'time'
|
|
3
|
-
require 'dentaku/string_casing'
|
|
4
3
|
require 'dentaku/token'
|
|
5
4
|
require 'dentaku/numeric_parser'
|
|
6
5
|
|
|
7
6
|
module Dentaku
|
|
8
7
|
class TokenScanner
|
|
9
|
-
extend StringCasing
|
|
10
|
-
|
|
11
8
|
DATE_TIME_REGEXP = /\d{2}\d{2}?-\d{1,2}-\d{1,2}([ |T]\d{1,2}:\d{1,2}:\d{1,2}(\.\d*)?)? ?(Z|((\+|\-)\d{2}\:?\d{2}))?(?!\d)/.freeze
|
|
12
9
|
|
|
13
10
|
def initialize(category, regexp, converter = nil, condition = nil)
|
|
@@ -31,8 +28,6 @@ module Dentaku
|
|
|
31
28
|
end
|
|
32
29
|
|
|
33
30
|
class << self
|
|
34
|
-
attr_reader :case_sensitive
|
|
35
|
-
|
|
36
31
|
def available_scanners
|
|
37
32
|
[
|
|
38
33
|
:null,
|
|
@@ -76,7 +71,6 @@ module Dentaku
|
|
|
76
71
|
end
|
|
77
72
|
|
|
78
73
|
def scanners(options = {})
|
|
79
|
-
@case_sensitive = options.fetch(:case_sensitive, false)
|
|
80
74
|
raw_date_literals = options.fetch(:raw_date_literals, true)
|
|
81
75
|
|
|
82
76
|
@scanners.select { |k, _| raw_date_literals || k != :datetime }.values
|
|
@@ -180,7 +174,7 @@ module Dentaku
|
|
|
180
174
|
end
|
|
181
175
|
|
|
182
176
|
def identifier
|
|
183
|
-
new(:identifier, '[[[:word:]]\.]+\b', lambda { |raw|
|
|
177
|
+
new(:identifier, '[[[:word:]]\.]+\b', lambda { |raw| raw.strip })
|
|
184
178
|
end
|
|
185
179
|
|
|
186
180
|
def quoted_identifier
|
data/lib/dentaku/tokenizer.rb
CHANGED
data/lib/dentaku/version.rb
CHANGED
data/spec/ast/xor_spec.rb
CHANGED
|
@@ -15,9 +15,22 @@ describe 'Dentaku::AST::Xor' do
|
|
|
15
15
|
expect(result).to eq(true)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
it 'returns false if
|
|
19
|
-
|
|
20
|
-
expect(
|
|
18
|
+
it 'returns false if an even number of the arguments are true' do
|
|
19
|
+
expect(Dentaku('XOR(false, true, true)')).to eq(false)
|
|
20
|
+
expect(Dentaku('XOR(true, true, true, true)')).to eq(false)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'returns true if an odd number of the arguments are true' do
|
|
24
|
+
expect(Dentaku('XOR(true, true, true)')).to eq(true)
|
|
25
|
+
expect(Dentaku('XOR(true, false, true, true)')).to eq(true)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'matches XOR applied pairwise' do
|
|
29
|
+
[true, false].repeated_permutation(4).each do |a, b, c, d|
|
|
30
|
+
context = { a: a, b: b, c: c, d: d }
|
|
31
|
+
expect(Dentaku('XOR(a, b, c, d)', context))
|
|
32
|
+
.to eq(Dentaku('XOR(XOR(a, b), XOR(c, d))', context))
|
|
33
|
+
end
|
|
21
34
|
end
|
|
22
35
|
|
|
23
36
|
it 'supports nested expressions' do
|
|
@@ -8,6 +8,28 @@ RSpec.describe Dentaku::BulkExpressionSolver do
|
|
|
8
8
|
let(:calculator) { Dentaku::Calculator.new }
|
|
9
9
|
|
|
10
10
|
describe "#solve!" do
|
|
11
|
+
it 'does not reuse a cached order after formulas change with the same variable names' do
|
|
12
|
+
calculator = Dentaku::Calculator.new(cache_dependency_order: true)
|
|
13
|
+
calculator.store(seed: 1)
|
|
14
|
+
expect(calculator.solve!(a: 'b + 1', b: 'seed + 1')).to eq(a: 3, b: 2)
|
|
15
|
+
expect(calculator.solve!(a: 'seed + 1', b: 'a + 1')).to eq(a: 2, b: 3)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'does not share a cached order with a calculator using different dependencies' do
|
|
19
|
+
first = Dentaku::Calculator.new(cache_dependency_order: true).store(seed: 1)
|
|
20
|
+
second = Dentaku::Calculator.new(cache_dependency_order: true).store(seed: 10)
|
|
21
|
+
expect(first.solve!(left: 'right + 1', right: 'seed + 1')).to eq(left: 3, right: 2)
|
|
22
|
+
expect(second.solve!(left: 'seed + 1', right: 'left + 1')).to eq(left: 11, right: 12)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'includes memory-dependent edges in the cached order' do
|
|
26
|
+
first = Dentaku::Calculator.new(cache_dependency_order: true).store(seed: 1, base: 5)
|
|
27
|
+
second = Dentaku::Calculator.new(cache_dependency_order: true).store(seed: 1)
|
|
28
|
+
formulas = { result: 'base + 1', base: 'seed + 1' }
|
|
29
|
+
expect(first.solve!(formulas)).to eq(result: 6, base: 2)
|
|
30
|
+
expect(second.solve!(formulas)).to eq(result: 3, base: 2)
|
|
31
|
+
end
|
|
32
|
+
|
|
11
33
|
it "evaluates properly with variables, even if some in memory" do
|
|
12
34
|
expressions = {
|
|
13
35
|
weekly_fruit_budget: "weekly_apple_budget + pear * 4",
|
data/spec/calculator_spec.rb
CHANGED
|
@@ -556,6 +556,14 @@ describe Dentaku::Calculator do
|
|
|
556
556
|
expect(calculator.evaluate!('value2 - value', { value: value, value2: value2 })).to eq(value2 - value)
|
|
557
557
|
expect(calculator.evaluate!('value - 7200', { value: value })).to eq(Time.local(2023, 7, 13, 8, 42, 11))
|
|
558
558
|
end
|
|
559
|
+
|
|
560
|
+
it 'does not accept unknown durations' do
|
|
561
|
+
expect { calculator.evaluate!('duration(1, foo)') }.to raise_error(Dentaku::ArgumentError, /is not a valid duration unit/)
|
|
562
|
+
expect { calculator.evaluate!('duration(1, ay)') }.to raise_error(Dentaku::ArgumentError, /is not a valid duration unit/)
|
|
563
|
+
expect { calculator.evaluate!('duration(1, daysy)') }.to raise_error(Dentaku::ArgumentError, /is not a valid duration unit/)
|
|
564
|
+
expect { calculator.evaluate!('duration(1, ayear)') }.to raise_error(Dentaku::ArgumentError, /is not a valid duration unit/)
|
|
565
|
+
expect { calculator.evaluate!('duration(1, dayofmonthofyear)') }.to raise_error(Dentaku::ArgumentError, /is not a valid duration unit/)
|
|
566
|
+
end
|
|
559
567
|
end
|
|
560
568
|
|
|
561
569
|
describe 'functions' do
|
|
@@ -1001,6 +1009,39 @@ describe Dentaku::Calculator do
|
|
|
1001
1009
|
end
|
|
1002
1010
|
end
|
|
1003
1011
|
|
|
1012
|
+
describe 'logical operators whose short-circuit probe raises' do
|
|
1013
|
+
it 'falls back to the dependencies of both operands' do
|
|
1014
|
+
expect(calculator.dependencies('a / b > 1 OR c', 'a' => 1, 'b' => 0)).to eq(['c'])
|
|
1015
|
+
expect(calculator.dependencies('a <= 1 OR c', 'a' => nil)).to eq(['c'])
|
|
1016
|
+
end
|
|
1017
|
+
|
|
1018
|
+
it 'still prunes when the probe is decisive' do
|
|
1019
|
+
expect(calculator.dependencies('1 > 2 AND a')).to eq([])
|
|
1020
|
+
expect(calculator.dependencies('1 < 2 OR a')).to eq([])
|
|
1021
|
+
expect(calculator.dependencies('a AND 1 > 2')).to eq([])
|
|
1022
|
+
expect(calculator.dependencies('a OR 1 < 2')).to eq([])
|
|
1023
|
+
end
|
|
1024
|
+
|
|
1025
|
+
it 'still reports the other operand when the probe is not decisive' do
|
|
1026
|
+
expect(calculator.dependencies('1 < 2 AND a')).to eq(['a'])
|
|
1027
|
+
expect(calculator.dependencies('1 > 2 OR a')).to eq(['a'])
|
|
1028
|
+
end
|
|
1029
|
+
|
|
1030
|
+
# Regression: the raising branch used to re-derive the dependency union via
|
|
1031
|
+
# `super`, walking both subtrees a second time at every level of the chain.
|
|
1032
|
+
# Twelve terms probed 2048 times instead of 12.
|
|
1033
|
+
it 'probes each operand of a chain once rather than exponentially' do
|
|
1034
|
+
call_count = { count: 0 }
|
|
1035
|
+
calculator.add_function(:blank, :numeric, -> { call_count[:count] += 1; nil })
|
|
1036
|
+
|
|
1037
|
+
terms = 12
|
|
1038
|
+
formula = (1..terms).map { |i| "(blank() <= #{i})" }.join(' OR ')
|
|
1039
|
+
|
|
1040
|
+
expect(calculator.evaluate(formula)).to be_nil
|
|
1041
|
+
expect(call_count[:count]).to eq(terms)
|
|
1042
|
+
end
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1004
1045
|
describe 'aliases' do
|
|
1005
1046
|
it 'accepts aliases as instance option' do
|
|
1006
1047
|
expect(with_aliases.evaluate('rrround(5.1)')).to eq(5)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku'
|
|
3
|
+
require 'dentaku/calculator'
|
|
4
|
+
|
|
5
|
+
describe Dentaku::Calculator do
|
|
6
|
+
let(:case_sensitive) { described_class.new(case_sensitive: true) }
|
|
7
|
+
let(:case_insensitive) { described_class.new }
|
|
8
|
+
|
|
9
|
+
it 'honours its own setting regardless of what other instances do' do
|
|
10
|
+
expect(case_sensitive.evaluate('Kiwi + 5', Kiwi: -2, kiwi: 2)).to eq(3)
|
|
11
|
+
expect(case_insensitive.evaluate('Kiwi + 5', Kiwi: -2, kiwi: 2)).to eq(7)
|
|
12
|
+
|
|
13
|
+
# same two settings, created and evaluated in the opposite order
|
|
14
|
+
other_sensitive = described_class.new(case_sensitive: true)
|
|
15
|
+
other_insensitive = described_class.new
|
|
16
|
+
expect(other_insensitive.evaluate('Kiwi + 5', Kiwi: -2, kiwi: 2)).to eq(7)
|
|
17
|
+
expect(other_sensitive.evaluate('Kiwi + 5', Kiwi: -2, kiwi: 2)).to eq(3)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Identifier casing used to be applied twice: once by the identifier scanner,
|
|
21
|
+
# reading a class-level flag on TokenScanner that every calculator in the
|
|
22
|
+
# process shared, and again by AST::Identifier using the calculator's own
|
|
23
|
+
# flag. The shared flag meant one instance could overwrite another's mode
|
|
24
|
+
# while the first was still tokenizing -- in practice, whenever two threads
|
|
25
|
+
# evaluated at the same time, identifiers silently folded to the other
|
|
26
|
+
# instance's casing.
|
|
27
|
+
#
|
|
28
|
+
# Reproduce that interleaving deterministically: once this calculator has
|
|
29
|
+
# fetched its scanners (which is when the mode used to be recorded), let a
|
|
30
|
+
# second, case-insensitive calculator tokenize before the first one scans its
|
|
31
|
+
# identifiers. The first calculator must be unaffected.
|
|
32
|
+
it 'is unaffected by another instance tokenizing while it scans' do
|
|
33
|
+
interloped = false
|
|
34
|
+
allow(Dentaku::TokenScanner).to receive(:scanners).and_wrap_original do |original, *args|
|
|
35
|
+
original.call(*args).tap do
|
|
36
|
+
next if interloped
|
|
37
|
+
|
|
38
|
+
interloped = true
|
|
39
|
+
case_insensitive.evaluate('Zz', Zz: 1)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
expect(case_sensitive.evaluate('Kiwi', Kiwi: -2, kiwi: 2)).to eq(-2)
|
|
44
|
+
expect(case_insensitive.evaluate('Kiwi', Kiwi: -2, kiwi: 2)).to eq(2)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# casing is applied in exactly one place, AST::Identifier, which is handed
|
|
48
|
+
# the mode of the calculator that owns the parse -- so the token stream keeps
|
|
49
|
+
# the source casing of every identifier
|
|
50
|
+
it 'leaves identifier casing to the parser' do
|
|
51
|
+
tokens = Dentaku::Tokenizer.new.tokenize('Kiwi + 5')
|
|
52
|
+
expect(tokens.first.value).to eq('Kiwi')
|
|
53
|
+
end
|
|
54
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dentaku
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.0
|
|
4
|
+
version: 4.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Solomon White
|
|
@@ -175,6 +175,7 @@ files:
|
|
|
175
175
|
- spec/benchmark.rb
|
|
176
176
|
- spec/bulk_expression_solver_spec.rb
|
|
177
177
|
- spec/calculator_spec.rb
|
|
178
|
+
- spec/case_sensitivity_spec.rb
|
|
178
179
|
- spec/dentaku_spec.rb
|
|
179
180
|
- spec/dependency_resolver_spec.rb
|
|
180
181
|
- spec/exceptions_spec.rb
|
|
@@ -215,7 +216,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
215
216
|
- !ruby/object:Gem::Version
|
|
216
217
|
version: '0'
|
|
217
218
|
requirements: []
|
|
218
|
-
rubygems_version:
|
|
219
|
+
rubygems_version: 4.0.20
|
|
219
220
|
specification_version: 4
|
|
220
221
|
summary: A formula language parser and evaluator
|
|
221
222
|
test_files: []
|