xel 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
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: []