xel 1.5.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: aff030adf152ce449c4cc1680640329458dfc4f53388af9218f3165fbe67e05b
4
+ data.tar.gz: e654443aeda26b28abad25e9be066132924415cab58b26a31b83e8121e421bc3
5
+ SHA512:
6
+ metadata.gz: c289c6eedf021d9065d6fd3169c4a8886e4a24765b80c7861a99485eba512fe88afa22de4766422e8cc16fcb36f0a541065d3d7e6bee1264e58b6ce39bea5501
7
+ data.tar.gz: 7f108af095671d99a9a58b47a35c4c561e61206f554872f85b4282f2aeb6e1147611b3f875235738e87cdb75357bcfb3de153c81a9fe39654f0fa0edddd1ca4e
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+
2
+ # xel.rb
3
+
4
+
5
+ ## xel.rb 1.5.1 not yet released
6
+
7
+ * Initial release
8
+
data/LICENSE.txt ADDED
@@ -0,0 +1,26 @@
1
+
2
+ #
3
+ # Copyright (c) 2015-2024, John Mettraux, jmettraux@gmail.com
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+ #
23
+ #
24
+ # Made in Japan
25
+ #
26
+
data/README.md ADDED
@@ -0,0 +1,265 @@
1
+
2
+ # xel.rb
3
+
4
+ Interpreting expressions built out of a subset of spreadsheet functions.
5
+
6
+ Twin to [xel.js](https://github.com/jmettraux/xel.js).
7
+
8
+ ```ruby
9
+ require 'xel'
10
+
11
+ Xel.eval(" = CASE(a<5000000, 0.6, a<10000000, 0.55, 0.45)", { a: 10000 })
12
+ # --> 0.6
13
+
14
+ Xel.eval("CASE(a<5000000, 0.6, a<10000000, 0.55, 0.45)", { a: 5_100_000 })
15
+ # --> 0.55
16
+ ```
17
+
18
+ Here are the eval tests, in the format `code [ ⟶ context] ⟶ result`
19
+
20
+ ```
21
+ 987 ⟶ 987
22
+ 3,000,000 ⟶ 3_000_000
23
+ 123.45 ⟶ 123.45
24
+ 9,090.10 ⟶ 9_090.1
25
+ .123 ⟶ 0.123
26
+ -987 ⟶ -987
27
+ -3,000,000 ⟶ -3_000_000
28
+ -123.45 ⟶ -123.45
29
+ -9,090.10 ⟶ -9_090.10
30
+ -.123 ⟶ -0.123
31
+
32
+ "12" =~ "[a-z]" ⟶ false
33
+ "a" != "a" ⟶ false
34
+ "a" != "b" ⟶ true
35
+ "a" = "a" ⟶ true
36
+ "a" = "b" ⟶ false
37
+ "a" IN { 0, "a", 2 } ⟶ true
38
+ "a" IN { 0, "b", 2 } ⟶ false
39
+ "a" IN {} ⟶ false
40
+ "ab" =~ "[a-z]" ⟶ true
41
+
42
+ "b" IN a ⟶ {"a":["a","b","c"]} ⟶ true
43
+ "d" IN a ⟶ {"a":["a","b","c"]} ⟶ false
44
+
45
+ { "a" } != { "a" } ⟶ false
46
+ { "a" } != { "b" } ⟶ true
47
+ { "a" } = { "a" } ⟶ true
48
+ { "a" } = { "b" } ⟶ false
49
+ { "a", "b" } != { "a" } ⟶ true
50
+ { "a", "b" } != { "a", "b" } ⟶ false
51
+ { "a", "b" } = { "a" } ⟶ false
52
+ { "a", "b" } = { "a", "b" } ⟶ true
53
+ { 1, "a", "b", "c" } ⟶ [ 1, "a", "b", "c" ]
54
+ { 1, 2 } + { "a", "b" } ⟶ [ 1, 2, "a", "b" ]
55
+ { { "a", "b" }, { "c", "de" } } ⟶ [ [ "a", "b" ], [ "c", "de" ] ]
56
+ { { "a", 1 }, { "c", 2 } } ⟶ [ [ "a", 1 ], [ "c", 2 ] ]
57
+ { "a", "b", "c" } ⟶ [ "a", "b", "c" ]
58
+ { "a","b" } ⟶ [ "a", "b" ]
59
+
60
+ LET(price, 100, count, 3, count * price) ⟶ 300
61
+ LET(price, 95, count, 3, 12, count * price) ⟶ 285
62
+ LET(price, 13, LOWER("COUNT"), 3, count * price) ⟶ 39
63
+ LET(l, LAMBDA(a, b, a + b), l(2, 3)) ⟶ 5
64
+ LET(_l, LAMBDA(a, b, a * b), _l(2, 3)) ⟶ 6
65
+
66
+ ROUND(3.14159) ⟶ 3
67
+ ROUND(3.14159, 0) ⟶ 3
68
+ ROUND(3.14159, 2) ⟶ 3.14
69
+ ROUND(157, -1) ⟶ 160
70
+ ROUND(157, -2) ⟶ 200
71
+ ROUND(2.678, 1) ⟶ 2.7
72
+
73
+ MROUND(51, 3) ⟶ 51
74
+ MROUND(50, 7) ⟶ 49
75
+ MROUND(50, 3) ⟶ 51
76
+ MROUND(10, 3) ⟶ 9
77
+ MROUND(10, -3) ⟶ nil
78
+ MROUND(-10, 3) ⟶ nil
79
+ MROUND(-10, -3) ⟶ -9
80
+ MROUND(0, 3) ⟶ 0
81
+ MROUND(10, 0) ⟶ nil
82
+ MROUND(10.5, 0.3) ⟶ 10.5
83
+ MROUND(5, 3) ⟶ 6
84
+ MROUND(4, 3) ⟶ 3
85
+ MROUND(10.5678, 0.05) ⟶ 10.55
86
+
87
+ # need MROUND2 for the job, but why again?
88
+ #
89
+ MROUND2(51, 3) ⟶ 51
90
+ MROUND2(50, 7) ⟶ 49
91
+ MROUND2(50, 3) ⟶ 51
92
+ MROUND2(10, 3) ⟶ 9
93
+ MROUND2(10, -3) ⟶ nil
94
+ MROUND2(-10, 3) ⟶ nil
95
+ MROUND2(-10, -3) ⟶ -9
96
+ MROUND2(0, 3) ⟶ 0
97
+ MROUND2(10, 0) ⟶ nil
98
+ MROUND2(10.5, 0.3) ⟶ 10.5
99
+ MROUND2(5, 3) ⟶ 6
100
+ MROUND2(4, 3) ⟶ 3
101
+ MROUND2(10.5678, 0.05) ⟶ 10.55
102
+
103
+ CEILING(7, 1) ⟶ 7
104
+ CEILING(7) ⟶ 7
105
+ CEILING(7.05, 1) ⟶ 8
106
+ CEILING(7.05) ⟶ 8
107
+ CEILING(7, 5) ⟶ 10
108
+ CEILING(49.25, 5) ⟶ 50
109
+ CEILING(501, 100) ⟶ 600
110
+ CEILING(550, 100) ⟶ 600
111
+ CEILING(10.1, 0.25) ⟶ 10.25
112
+
113
+ FLOOR(7, 5) ⟶ 5
114
+ FLOOR(49.25) ⟶ 49
115
+ FLOOR(49.25, 1) ⟶ 49
116
+ FLOOR(550, 100) ⟶ 500
117
+ FLOOR(10.1, 0.25) ⟶ 10
118
+
119
+ TRUNC(8.9) ⟶ 8
120
+ TRUNC(8.9, 0) ⟶ 8
121
+ TRUNC(3.14159, 2) ⟶ 3.14
122
+ TRUNC(2.678, 1) ⟶ 2.6
123
+
124
+ "ab\"cd'ef" ⟶ "ab\"cd'ef"
125
+ 'ab"cd\'ef' ⟶ "ab\"cd'ef"
126
+
127
+ 0.45 * we_c - 0.15 * it_c + 0.15 * sm_c + 0.25 * gp_c ⟶ \
128
+ {:we_c=>1.1, :it_c=>2.2, :sm_c=>3.3, :gp_c=>4.4} ⟶ 1.76
129
+
130
+ 1 + "a" ⟶ "1a"
131
+ 1 + 2 ⟶ 3
132
+ 1 + a.b.c ⟶ {:a=>{:b=>{:c=>66}}} ⟶ 67
133
+ 1 + v0 ⟶ {:v0=>70} ⟶ 71
134
+ 1 - -2 ⟶ 3
135
+ 1 - 2 ⟶ -1
136
+ 1 / 5 ⟶ 0.2
137
+ "a" & "bc" ⟶ "abc"
138
+ 1 & 1 ⟶ "11"
139
+ "ab" & c & d & "ef" ⟶ {:c=>"c"} ⟶ "abcef"
140
+ 1 < 2 ⟶ true
141
+ 1 <= 2 ⟶ true
142
+ 1 IN a ⟶ {:a=>[0, 1, 2]} ⟶ true
143
+ 1 IN { 0, 1 } ⟶ true
144
+ 1.0 / 5 ⟶ 0.2
145
+ 2 != 3 ⟶ true
146
+ 2 <= 2 ⟶ true
147
+ 2 >= 2 ⟶ true
148
+ 3 = 3 ⟶ true
149
+ 3 >= 2 ⟶ true
150
+
151
+ AND(TRUE()) ⟶ true
152
+ AND(TRUE(), TRUE()) ⟶ true
153
+
154
+ CASE(AND(a > 99, 2 > 1), 2, a > 9, 1, -1) ⟶ {:a=>100} ⟶ 2
155
+ CASE(a > 99, 2, a > 9, 1, -1) ⟶ {:a=>100} ⟶ 2
156
+ CASE(eb, "&", 10, "g", 11, "a", 12, 13) ⟶ {:eb=>"nada"} ⟶ 13
157
+
158
+ COUNTA(a) ⟶ {:a=>[]} ⟶ 0
159
+ COUNTA(a) ⟶ {:a=>[1, 2, 3]} ⟶ 3
160
+
161
+ HAS(a, "b") ⟶ {:a=>["a", "b", "c"]} ⟶ true
162
+ HAS(a, 1) ⟶ {:a=>[0, 1, 2]} ⟶ true
163
+
164
+ IF(FALSE(), 1, 2) ⟶ 2
165
+ IF(TRUE(), 1, 2) ⟶ 1
166
+ IF(f, 1, 2) ⟶ {:f=>false} ⟶ 2
167
+ IF(t, 1, 2) ⟶ {:t=>true} ⟶ 1
168
+
169
+ INDEX(a, -1) ⟶ {:a=>[0, 1, 2, "trois"]} ⟶ "trois"
170
+ INDEX(a, -2) ⟶ {:a=>[0, 1, 2, "trois"]} ⟶ 2
171
+ INDEX(a, 1) ⟶ {:a=>[0, 1, 2]} ⟶ 0
172
+ INDEX(a, 2) ⟶ {:a=>[0, 1, 2]} ⟶ 1
173
+ INDEX(a, COUNTA(a)) ⟶ {:a=>[0, "two"]} ⟶ "two"
174
+ INDEX({ 'ab', 'cd', 'ef' }, -2) ⟶ "cd"
175
+
176
+ ISBLANK(a) ⟶ {:a=>""} ⟶ true
177
+ ISBLANK(a) ⟶ {} ⟶ true
178
+
179
+ ISNUMBER(123) ⟶ true
180
+ ISNUMBER(123.12) ⟶ true
181
+
182
+ LN(3044.31) ⟶ 8.02
183
+ LN(a) ⟶ {:a=>[3044.31, 3047.12]} ⟶ [8.02, 8.02]
184
+ LN({ 3044.31, 3047.12 }) ⟶ [8.02, 8.02]
185
+
186
+ MATCH("b", a, 0) ⟶ {:a=>["a", "b", "c"]} ⟶ 1
187
+ MATCH("d", a, 0) ⟶ {:a=>["a", "b"]} ⟶ -1
188
+ MATCH(1, a, 0) ⟶ {:a=>[0, 1, 2]} ⟶ 1
189
+
190
+ MAX(-1, -2, "a", -3) ⟶ -1
191
+ MAX(-1, -2, -3) ⟶ -1
192
+ MAX(1, 2, 3) ⟶ 3
193
+ MIN(-1, -2, "a", -3) ⟶ -1
194
+ MIN(-1, -2, -3) ⟶ -3
195
+ MIN(1, 2, 3) ⟶ 1
196
+ NOT(FALSE()) ⟶ true
197
+
198
+ OR(1 = 2, 2 = 2) ⟶ true
199
+ OR(TRUE(), FALSE()) ⟶ true
200
+
201
+ PRODUCT(2, 3, 4) ⟶ 24
202
+ PRODUCT({ 2, 3, 4 }) ⟶ 24
203
+ PRODUCT({ 2, 3, 4 }, 2) ⟶ 48
204
+ PRODUCT({ 2, 3, 4 }, a, 2) ⟶ {:a=>[0.5, 0.5]} ⟶ 12
205
+
206
+ PROPER("alpha bravo charly") ⟶ "Alpha Bravo Charly"
207
+
208
+ SORT({ 1, "aa", 7, 2 }, 1, -1) ⟶ ["aa", 7, 2, 1]
209
+ SORT({ 1, 3, 2 }) ⟶ [1, 2, 3]
210
+ SORT({ 1, 3, 2 }, 1, -1) ⟶ [3, 2, 1]
211
+
212
+ SQRT(260) ⟶ 16.1245
213
+ SQRT(a) ⟶ {:a=>[260, 81]} ⟶ [16.1245, 9]
214
+ SQRT({ 260, 81 }) ⟶ [16.1245, 9]
215
+
216
+ STDEV(a) ⟶ {:a=>[10, 11]} ⟶ 0.71
217
+
218
+ SUM(2, 3, 4) ⟶ 9
219
+ SUM({ 2, 3, 4 }) ⟶ 9
220
+ SUM({ 2, 3, 4 }, 2) ⟶ 11
221
+ SUM({ 2, 3, 4 }, a, 2) ⟶ {:a=>[0.5, 0.5]} ⟶ 12
222
+
223
+ TRUE() ⟶ true
224
+
225
+ UNIQUE(a) ⟶ {:a=>[1, 2, 1, 1, 2, 3]} ⟶ [1, 2, 3]
226
+ UNIQUE({ 1, 1 }) ⟶ [1]
227
+
228
+ UPPER("alpha bravo charly") ⟶ "ALPHA BRAVO CHARLY"
229
+ LOWER("ALPHA BRAVO Charly") ⟶ "alpha bravo charly"
230
+
231
+ a != "" ⟶ {:a=>"abc"} ⟶ true
232
+
233
+ LAMBDA(a, b, a + b) ⟶ {}
234
+
235
+ KALL(LAMBDA(a, b, a + b), 7, -3) ⟶ 4
236
+ KALL(LAMBDA(a, b, a + b), 7, -2, 1) ⟶ 5
237
+
238
+ MAP({ 2, 3, 4 }, LAMBDA(a, 2 * a)) ⟶ [4, 6, 8]
239
+
240
+ REDUCE(0, { 2, 3, 4 }, LAMBDA(a, e, a + e)) ⟶ 9
241
+ REDUCE({ 2, 3, 5 }, LAMBDA(a, e, a + e)) ⟶ 10
242
+
243
+ ORV('', '', 1) ⟶ 1
244
+ ORV('', b, a, 3) ⟶ {:a=>2} ⟶ 2
245
+
246
+ TEXTJOIN("/", TRUE(), "a", "b") ⟶ "a/b"
247
+ TEXTJOIN(", ", TRUE(), a, "zz") ⟶ {:a=>["ab", "cd", "ef1"]} ⟶ "ab, cd, ef1, zz"
248
+ TEXTJOIN(", ", TRUE(), a, "zz") ⟶ {:a=>["ab", "", "ef1"]} ⟶ "ab, ef1, zz"
249
+ TEXTJOIN(", ", FALSE(), a, "zz") ⟶ {:a=>["ab", "", "ef1"]} ⟶ "ab, , ef1, zz"
250
+
251
+ D() ⟶ \
252
+ {}
253
+ D('alpha') ⟶ \
254
+ { alpha: nil }
255
+ D('alpha', 'bravo') ⟶ \
256
+ { alpha: 'bravo' }
257
+ D('a', 1, 'b', 'deux', 'charly', { 1, 'deux' }) ⟶ \
258
+ { a: 1, b: 'deux', charly: [ 1, 'deux' ] }
259
+ ```
260
+
261
+
262
+ ## LICENSE
263
+
264
+ MIT, see [LICENSE.txt](LICENSE.txt)
265
+
@@ -0,0 +1,530 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ module Xel
5
+
6
+ # eval_XXX
7
+
8
+ class << self
9
+
10
+ protected
11
+
12
+ def _eval_args(tree, context, opts={})
13
+
14
+ sta = opts[:start] || 1
15
+ max = opts[:max] || 99
16
+
17
+ a = []
18
+ tree[sta..-1].each do |t|
19
+ break if a.length >= max
20
+ a << do_eval(t, context)
21
+ end
22
+
23
+ a
24
+ end
25
+
26
+ def eval_str(tree, context); tree[1]; end
27
+
28
+ def eval_num(tree, context)
29
+
30
+ s = tree[1].gsub(',', '')
31
+
32
+ s.index('.') ? s.to_f : s.to_i
33
+ end
34
+
35
+ def eval_var(tree, context)
36
+
37
+ tree[1].split('.')
38
+ .inject(context) { |r, k|
39
+ if r && r.respond_to?(:has_key?)
40
+ ks = k.to_sym
41
+ if r.has_key?(k)
42
+ r[k]
43
+ elsif r.has_key?(ks)
44
+ r[ks]
45
+ else
46
+ nil
47
+ end
48
+ else
49
+ nil
50
+ end }
51
+ end
52
+
53
+ def eval_inv(tree, context)
54
+ 1.0 / do_eval(tree[1], context)
55
+ end
56
+ def eval_opp(tree, context)
57
+ - do_eval(tree[1], context)
58
+ end
59
+
60
+ def eval_bool(tree, context); tree[0].downcase == 'true'; end
61
+
62
+ def do_eval_equal(sign, a0, a1)
63
+
64
+ a0 = '' if a0 == nil
65
+ a1 = '' if a1 == nil
66
+
67
+ sign == '=' ? a0 == a1 : a0 != a1
68
+ end
69
+
70
+ def eval_cmp(tree, context)
71
+
72
+ args = _eval_args(tree, context, start: 2)
73
+
74
+ case tree[1]
75
+ when '=', '!=' then do_eval_equal(tree[1], args[0], args[1])
76
+ when '>' then args[0] > args[1]
77
+ when '<' then args[0] < args[1]
78
+ when '>=' then args[0] >= args[1]
79
+ when '<=' then args[0] <= args[1]
80
+ when '=~' then !! args[0].to_s.match(args[1].to_s)
81
+ when 'IN' then args[1].include?(args[0])
82
+ else false
83
+ end
84
+
85
+ rescue
86
+ false
87
+ end
88
+
89
+ def eval_TRUE(tree, context); tree[0] == 'TRUE'; end
90
+ alias eval_FALSE eval_TRUE
91
+
92
+ alias eval_arr _eval_args
93
+
94
+ def eval_plus(tree, context)
95
+
96
+ args = _eval_args(tree, context)
97
+
98
+ if args[0].is_a?(Array)
99
+ args.inject([]) { |a, arg| a.concat(arg) }
100
+ elsif args.find { |a| ! a.is_a?(Numeric) }
101
+ args.map(&:to_s).join
102
+ elsif args.all? { |a| a.is_a?(Numeric) }
103
+ args.inject(&:+)
104
+ else
105
+ nil
106
+ end
107
+ end
108
+
109
+ def eval_amp(tree, context)
110
+
111
+ _eval_args(tree, context, start: 1).collect(&:to_s).join
112
+ end
113
+
114
+ def eval_AND(tree, context)
115
+
116
+ ! tree[1..-1].find { |c| do_eval(c, context) != true }
117
+ end
118
+
119
+ def eval_OR(tree, context)
120
+
121
+ !! tree[1..-1].find { |c| do_eval(c, context) == true }
122
+ end
123
+
124
+ def eval_NOT(tree, context)
125
+
126
+ ! do_eval(tree[1], context)
127
+ end
128
+
129
+ def eval_ORV(tree, context)
130
+
131
+ tree[1..-1].each do |t|
132
+ v = do_eval(t, context)
133
+ return v if v != '' && v!= nil
134
+ end
135
+
136
+ nil
137
+ end
138
+
139
+ def eval_IF(tree, context)
140
+
141
+ return do_eval(tree[2], context) if do_eval(tree[1], context)
142
+ do_eval(tree[3], context)
143
+ end
144
+
145
+ def eval_CASE(tree, context)
146
+
147
+ control = do_eval(tree[1], context)
148
+ args = tree[2..-1]
149
+
150
+ if control == true || control == false
151
+ args.unshift(control)
152
+ control = true
153
+ end
154
+
155
+ default = args.size.odd? ? args.pop : nil
156
+
157
+ while (ab = args.shift(2)).any?
158
+ return do_eval(ab[1], context) if control == do_eval(ab[0], context)
159
+ end
160
+ do_eval(default, context)
161
+ end
162
+ alias eval_SWITCH eval_CASE
163
+
164
+ def eval_UNIQUE(tree, context)
165
+
166
+ arr = do_eval(tree[1], context)
167
+
168
+ fail ArgumentError.new("UNIQUE() expects array not #{arr.class}") \
169
+ unless arr.is_a?(Array)
170
+
171
+ arr.uniq
172
+ end
173
+
174
+ # SORT({ 1, 3, 2 }) --> [ 1, 2, 3 ]
175
+ # SORT({ 1, 3, 2 }, 1, -1) --> [ 3, 2, 1 ]
176
+ #
177
+ def eval_SORT(tree, context)
178
+
179
+ #arr, col, dir = _eval_args(tree, context, max: 3)
180
+ arr, _, dir = _eval_args(tree, context, max: 3)
181
+
182
+ fail ArgumentError.new("SORT() expects array not #{arr.class}") \
183
+ unless arr.is_a?(Array)
184
+
185
+ r =
186
+ arr.all? { |e| e.is_a?(Numeric) } ? arr.sort :
187
+ arr.sort_by(&:to_s)
188
+
189
+ dir == -1 ? r.reverse : r
190
+ end
191
+
192
+ def eval_MUL(tree, context)
193
+
194
+ args = _eval_args(tree, context)
195
+
196
+ if args.find { |a| ! (a.is_a?(Integer) || a.is_a?(Float)) }
197
+ fail ArgumentError.new("cannot multiply #{args.inspect}")
198
+ end
199
+
200
+ args.reduce(&:*)
201
+ end
202
+
203
+ def eval_SUM(tree, context)
204
+
205
+ f = lambda { |r, e|
206
+ case e
207
+ when Numeric then r + e
208
+ when Array then e.inject(r, &f)
209
+ else r; end }
210
+
211
+ _eval_args(tree, context).inject(0, &f);
212
+ end
213
+
214
+ def eval_PRODUCT(tree, context)
215
+
216
+ f = lambda { |r, e|
217
+ case e
218
+ when Numeric then r * e
219
+ when Array then e.inject(r, &f)
220
+ else r; end }
221
+
222
+ _eval_args(tree, context).inject(1, &f)
223
+ end
224
+
225
+ def eval_MIN(tree, context)
226
+
227
+ args = _eval_args(tree, context)
228
+
229
+ if args.find { |a| ! (a.is_a?(Integer) || a.is_a?(Float)) }
230
+ args.first
231
+ else
232
+ args.min
233
+ end
234
+ end
235
+
236
+ def eval_MAX(tree, context)
237
+
238
+ args = _eval_args(tree, context)
239
+
240
+ if args.find { |a| ! (a.is_a?(Integer) || a.is_a?(Float)) }
241
+ args.first
242
+ else
243
+ args.max
244
+ end
245
+ end
246
+
247
+ def eval_MATCH(tree, context)
248
+
249
+ elt = do_eval(tree[1], context)
250
+ arr = do_eval(tree[2], context)
251
+
252
+ return -1 unless arr.is_a?(Array)
253
+ arr.index(elt) || -1
254
+ end
255
+
256
+ def eval_HAS(tree, context)
257
+
258
+ col = do_eval(tree[1], context)
259
+ elt = do_eval(tree[2], context)
260
+
261
+ return !! col.index(elt) if col.is_a?(Array)
262
+ return col.has_key?(elt) if col.is_a?(Hash)
263
+ false
264
+ end
265
+
266
+ def eval_INDEX(tree, context)
267
+
268
+ col = do_eval(tree[1], context)
269
+ i = do_eval(tree[2], context)
270
+
271
+ return 0 unless col.is_a?(Array)
272
+ return 0 unless i.is_a?(Numeric)
273
+
274
+ i < 0 ?
275
+ col[i] :
276
+ col[i.to_i - 1]
277
+ end
278
+
279
+ def eval_COUNTA(tree, context)
280
+
281
+ col = do_eval(tree[1], context)
282
+
283
+ col.is_a?(Array) ? col.length : 0
284
+ end
285
+
286
+ def eval_ISBLANK(tree, context)
287
+
288
+ val = do_eval(tree[1], context)
289
+
290
+ val == '' || val == nil
291
+ end
292
+
293
+ def eval_ISNUMBER(tree, context)
294
+
295
+ do_eval(tree[1], context).is_a?(Numeric)
296
+ end
297
+
298
+ def eval_PROPER(tree, context)
299
+ do_eval(tree[1], context).gsub(/(^|[^a-z])([a-z])/) { $1 + $2.upcase }
300
+ end
301
+ def eval_LOWER(tree, context)
302
+ do_eval(tree[1], context).downcase
303
+ end
304
+ def eval_UPPER(tree, context)
305
+ do_eval(tree[1], context).upcase
306
+ end
307
+
308
+ def eval_LN(tree, context)
309
+ a = do_eval(tree[1], context)
310
+ return a.map { |e| Math.log(e) } if a.is_a?(Array)
311
+ Math.log(a)
312
+ end
313
+ def eval_SQRT(tree, context)
314
+ a = do_eval(tree[1], context)
315
+ return a.map { |e| Math.sqrt(e) } if a.is_a?(Array)
316
+ Math.sqrt(a)
317
+ end
318
+
319
+ def eval_LET(tree, context)
320
+
321
+ ctx = context.dup
322
+
323
+ key = nil
324
+ #
325
+ tree[1..-2].each_with_index do |t, i|
326
+ if i % 2 == 0
327
+ key = (t[0] == 'var') ? t[1] : do_eval(t, ctx).to_s
328
+ else
329
+ ctx[key] = do_eval(t, ctx)
330
+ end
331
+ end
332
+
333
+ do_eval(tree[-1], ctx)
334
+ end
335
+
336
+ def eval_ROUND(tree, context)
337
+
338
+ args = _eval_args(tree, context, max: 2)
339
+ args << 0 if args.length < 2
340
+
341
+ args[0].round(args[1])
342
+ end
343
+
344
+ def eval_MROUND(tree, context)
345
+
346
+ n, m = _eval_args(tree, context, max: 2)
347
+
348
+ return Float::NAN if (n * m) < 0
349
+
350
+ (n.to_f / m).round * m rescue nil
351
+ end
352
+
353
+ alias eval_MROUND2 eval_MROUND
354
+
355
+ def eval_CEILING(tree, context)
356
+
357
+ as = _eval_args(tree, context, max: 2)
358
+ as << 1 if as.length < 2
359
+ n, m = as
360
+ r = n % m
361
+
362
+ r == 0 ? n : n - r + m
363
+ end
364
+
365
+ def eval_FLOOR(tree, context)
366
+
367
+ as = _eval_args(tree, context, max: 2)
368
+ as << 1 if as.length < 2
369
+ n, m = as
370
+
371
+ n - (n % m)
372
+ end
373
+
374
+ def eval_TRUNC(tree, context)
375
+
376
+ as = _eval_args(tree, context, max: 2)
377
+ as << 0 if as.length < 2
378
+ n = as[0]; m = 10 ** as[1]
379
+
380
+ (n * m).floor.to_f / m
381
+ end
382
+
383
+ def p2(n); n * n; end
384
+
385
+ def eval_STDEV(tree, context)
386
+
387
+ a = do_eval(tree[1], context)
388
+ s = a.inject(0.0) { |acc, e| acc + e }
389
+ m = s / a.length
390
+ s = a.inject(0.0) { |acc, e| acc + p2(e - m) }
391
+ v = s / (a.length - 1)
392
+
393
+ Math.sqrt(v)
394
+ end
395
+
396
+ def eval_VLOOKUP(tree, context)
397
+
398
+ k, t, i = _eval_args(tree, context, max: 3)
399
+
400
+ fail ArgumentError.new(
401
+ "VLOOKUP() arg 3 #{tree[3].inspect} is not an integer"
402
+ ) unless i.is_a?(Integer)
403
+
404
+ fail ArgumentError.new(
405
+ "VLOOKUP() arg 2 #{tree[2].inspect} doesn't point to an array of arrays"
406
+ ) unless t.is_a?(Array)
407
+
408
+ t.each_with_index do |r, j|
409
+
410
+ fail ArgumentError.new(
411
+ "VLOOKUP() arg2 row #{j + 1} of table is not an array"
412
+ ) unless r.is_a?(Array)
413
+
414
+ return r[i - 1] if r[0] == k
415
+ end
416
+
417
+ nil
418
+ end
419
+
420
+ def eval_LAMBDA(tree, context)
421
+
422
+ args = tree[1..-1].collect { |t| t[1] }
423
+ code = tree[-1]
424
+
425
+ l =
426
+ Proc.new do |*argl|
427
+ ctx = context.dup.merge(argl.pop)
428
+ args.each_with_index { |arg, i| ctx[arg] = argl[i] }
429
+ Xel.do_eval(code, ctx)
430
+ end
431
+ class << l; attr_accessor :_source; end
432
+ l._source = tree._source
433
+
434
+ l
435
+ end
436
+
437
+ def eval_KALL(tree, context)
438
+
439
+ args = _eval_args(tree, context)
440
+ args << context
441
+
442
+ fun = args.shift
443
+
444
+ fun.call(*args)
445
+ end
446
+
447
+ def eval_MAP(tree, context)
448
+
449
+ arr, fun = _eval_args(tree, context, max: 2)
450
+
451
+ arr.collect { |e| fun.call(e, context) }
452
+ end
453
+
454
+ def eval_REDUCE(tree, context)
455
+
456
+ ts = tree[1..-1]
457
+ fun = do_eval(ts.pop, context)
458
+ v0 = do_eval(ts[0], context)
459
+
460
+ acc, arr = nil
461
+ if ts.length == 1
462
+ arr = v0
463
+ acc = arr.shift
464
+ else
465
+ acc = v0
466
+ arr = do_eval(ts[1], context)
467
+ end
468
+
469
+ arr.inject(acc) { |r, e| fun.call(r, e, context) }
470
+ end
471
+
472
+ def eval_TEXTJOIN(tree, context)
473
+
474
+ agg = lambda { |acc, x|
475
+ case x
476
+ when String then acc << x.strip
477
+ when Array then x.each { |e| agg.call(acc, e) }
478
+ when nil then acc << ''
479
+ else acc << x.inspect; end
480
+ acc }
481
+
482
+ del, ign = _eval_args(tree, context, max: 2)
483
+
484
+ txs = tree[3..-1].inject([]) { |r, t| agg.call(r, do_eval(t, context)) }
485
+
486
+ txs = txs.select { |t| t.length > 0 } if ign
487
+
488
+ txs.join(del)
489
+ end
490
+
491
+ def eval_D(tree, context)
492
+
493
+ _eval_args(tree, context)
494
+ .each_slice(2)
495
+ .inject({}) { |h, (k, v)| h[k] = v; h }
496
+ end
497
+
498
+ def do_eval(tree, context={})
499
+
500
+ return tree unless tree.is_a?(Array) && tree.first.class == String
501
+
502
+ t0 = tree[0]
503
+
504
+ if (v = context[t0]) && v.is_a?(Proc)
505
+ args = _eval_args(tree, context)
506
+ args << context
507
+ return v.call(*args)
508
+ end
509
+
510
+ cfs = context['_custom_functions'] || context[:_custom_functions]
511
+
512
+ if (v = cfs && (cfs[t0] || cfs[t0.to_sym])) && v.is_a?(Proc)
513
+ return v.call(tree, context)
514
+ end
515
+
516
+ send("eval_#{t0}", tree, context)
517
+ end
518
+
519
+ public
520
+
521
+ def eval(s, context={})
522
+
523
+ t = s.is_a?(Array) ? s : Xel::Parser.parse(s)
524
+ fail ArgumentError.new("syntax error in >>#{s}<<") unless t
525
+
526
+ do_eval(t, context)
527
+ end
528
+ end
529
+ end
530
+
data/lib/xel/parser.rb ADDED
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ module Xel::Parser include Raabro
5
+
6
+ # parse
7
+
8
+ def aa(i); rex(nil, i, /\{\s*/); end
9
+ def az(i); rex(nil, i, /\}\s*/); end
10
+ def pa(i); rex(nil, i, /\(\s*/); end
11
+ def pz(i); rex(nil, i, /\)\s*/); end
12
+ def com(i); rex(nil, i, /,\s*/); end
13
+
14
+ def number(i)
15
+ rex(:number, i, /-?(\.[0-9]+|([0-9][,0-9]*[0-9]|[0-9]+)(\.[0-9]+)?)\s*/)
16
+ end
17
+
18
+ def var(i); rex(:var, i, /[a-z_][A-Za-z0-9_.]*\s*/); end
19
+
20
+ def arr(i); eseq(:arr, i, :aa, :cmp, :com, :az); end
21
+
22
+ def qstring(i); rex(:qstring, i, /'(\\'|[^'])*'\s*/); end
23
+ def dqstring(i); rex(:dqstring, i, /"(\\"|[^"])*"\s*/); end
24
+ def string(i); alt(:string, i, :dqstring, :qstring); end
25
+
26
+ def funargs(i); eseq(:funargs, i, :pa, :cmp, :com, :pz); end
27
+ def funname(i); rex(:funname, i, /[_a-zA-Z][_a-zA-Z0-9]*/); end
28
+ def fun(i); seq(:fun, i, :funname, :funargs); end
29
+
30
+ def comparator(i); rex(:comparator, i, /([\<\>]=?|=~|!?=|IN)\s*/); end
31
+ def multiplier(i); rex(:multiplier, i, /[*\/]\s*/); end
32
+ def adder(i); rex(:adder, i, /[+\-&]\s*/); end
33
+
34
+ def par(i); seq(:par, i, :pa, :cmp, :pz); end
35
+ def exp(i); alt(:exp, i, :par, :fun, :number, :string, :arr, :var); end
36
+
37
+ def mul(i); jseq(:mul, i, :exp, :multiplier); end
38
+ def add(i); jseq(:add, i, :mul, :adder); end
39
+
40
+ def rcmp(i); seq(:rcmp, i, :comparator, :add); end
41
+ def cmp(i); seq(:cmp, i, :add, :rcmp, '?'); end
42
+
43
+ def prequal(i); rex(nil, i, /\s*=?\s*/); end
44
+ def root(i); seq(nil, i, :prequal, :cmp); end
45
+
46
+ # rewrite
47
+
48
+ def rewrite_cmp(tree)
49
+
50
+ return rewrite(tree.children.first) if tree.children.size == 1
51
+
52
+ [ 'cmp',
53
+ tree.children[1].children.first.string.strip,
54
+ rewrite(tree.children[0]),
55
+ rewrite(tree.children[1].children[1]) ]
56
+ end
57
+
58
+ def rewrite_add(tree)
59
+
60
+ return rewrite(tree.children.first) if tree.children.size == 1
61
+
62
+ cn = tree.children.dup
63
+ a = [ tree.name == :add ? 'plus' : 'MUL' ]
64
+ a = [ 'amp' ] if cn[1] && cn[1].strinp == '&'
65
+ mod = nil
66
+
67
+ while c = cn.shift
68
+ v = rewrite(c)
69
+ v = [ mod, v ] if mod
70
+ a << v
71
+ c = cn.shift
72
+ break unless c
73
+ mod = { '-' => 'opp', '/' => 'inv' }[c.string.strip]
74
+ end
75
+
76
+ a
77
+ end
78
+ alias rewrite_mul rewrite_add
79
+
80
+ def rewrite_fun(tree)
81
+
82
+ t =
83
+ [ tree.children[0].string ] +
84
+ tree.children[1].children.select(&:name).collect { |c| rewrite(c) }
85
+ class << t; attr_accessor :_source; end
86
+ t._source = tree.strinp
87
+
88
+ t
89
+ end
90
+
91
+ def rewrite_exp(tree); rewrite(tree.children[0]); end
92
+ def rewrite_par(tree); rewrite(tree.children[1]); end
93
+
94
+ def rewrite_arr(tree)
95
+
96
+ [ 'arr',
97
+ *tree.children.inject([]) { |a, c| a << rewrite(c) if c.name; a } ]
98
+ end
99
+
100
+ def rewrite_var(tree); [ 'var', tree.string.strip ]; end
101
+ def rewrite_number(tree); [ 'num', tree.string.strip ]; end
102
+
103
+ def rewrite_string(tree)
104
+
105
+ s = tree.children[0].string.strip
106
+ q = s[0]
107
+ s = s[1..-2]
108
+
109
+ [ 'str', q == '"' ? s.gsub("\\\"", '"') : s.gsub("\\'", "'") ]
110
+ end
111
+ end
112
+
data/lib/xel.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ module Xel
5
+
6
+ VERSION = '1.5.1'
7
+ end
8
+
9
+ require 'raabro'
10
+
11
+ require 'xel/parser'
12
+ require 'xel/evaluator'
13
+
14
+
15
+ module Xel
16
+
17
+ class << self
18
+
19
+ def parse(s)
20
+
21
+ Xel::Parser.parse(s)
22
+ end
23
+ end
24
+ end
25
+
data/xel.gemspec ADDED
@@ -0,0 +1,46 @@
1
+
2
+ Gem::Specification.new do |s|
3
+
4
+ s.name = 'xel'
5
+
6
+ s.version = File.read(
7
+ File.expand_path('../lib/xel.rb', __FILE__)
8
+ ).match(/ VERSION *= *['"]([^'"]+)/)[1]
9
+
10
+ s.platform = Gem::Platform::RUBY
11
+ s.authors = [ 'John Mettraux' ]
12
+ s.email = [ 'jmettraux@gmail.com' ]
13
+ s.homepage = 'https://github.com/jmettraux/xel'
14
+ s.license = 'MIT'
15
+ s.summary = 'interprets the expressions usually found in a spreadsheet cell'
16
+
17
+ s.description = %{
18
+ Xel interprets the expressions usually found in a spreadsheet cell, hence its diminutive name.
19
+ }.strip
20
+
21
+ s.metadata = {
22
+ 'changelog_uri' => s.homepage + '/blob/master/CHANGELOG.md',
23
+ 'documentation_uri' => s.homepage,
24
+ 'bug_tracker_uri' => s.homepage + '/issues',
25
+ #'mailing_list_uri' => 'https://groups.google.com/forum/#!forum/floraison',
26
+ 'homepage_uri' => s.homepage,
27
+ 'source_code_uri' => s.homepage,
28
+ #'wiki_uri' => s.homepage + '/wiki',
29
+ }
30
+
31
+ #s.files = `git ls-files`.split("\n")
32
+ s.files = Dir[
33
+ 'README.{md,txt}',
34
+ 'CHANGELOG.{md,txt}', 'CREDITS.{md,txt}', 'LICENSE.{md,txt}',
35
+ #'Makefile',
36
+ 'lib/**/*.rb', #'spec/**/*.rb', 'test/**/*.rb',
37
+ "#{s.name}.gemspec",
38
+ ]
39
+
40
+ s.add_runtime_dependency 'raabro', '~> 1.4'
41
+
42
+ s.add_development_dependency 'rspec', '~> 3.8'
43
+
44
+ s.require_path = 'lib'
45
+ end
46
+
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xel
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.5.1
5
+ platform: ruby
6
+ authors:
7
+ - John Mettraux
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-07-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: raabro
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.8'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.8'
41
+ description: Xel interprets the expressions usually found in a spreadsheet cell, hence
42
+ its diminutive name.
43
+ email:
44
+ - jmettraux@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - CHANGELOG.md
50
+ - LICENSE.txt
51
+ - README.md
52
+ - lib/xel.rb
53
+ - lib/xel/evaluator.rb
54
+ - lib/xel/parser.rb
55
+ - xel.gemspec
56
+ homepage: https://github.com/jmettraux/xel
57
+ licenses:
58
+ - MIT
59
+ metadata:
60
+ changelog_uri: https://github.com/jmettraux/xel/blob/master/CHANGELOG.md
61
+ documentation_uri: https://github.com/jmettraux/xel
62
+ bug_tracker_uri: https://github.com/jmettraux/xel/issues
63
+ homepage_uri: https://github.com/jmettraux/xel
64
+ source_code_uri: https://github.com/jmettraux/xel
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 3.4.10
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: interprets the expressions usually found in a spreadsheet cell
84
+ test_files: []