furud 0.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.
@@ -0,0 +1,794 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "time"
5
+ require "set"
6
+
7
+ module Furud
8
+ module Functions
9
+ Entry = Struct.new(:name, :arity, :volatile, :implementation, keyword_init: true) do
10
+ ERROR_HANDLERS = %w[IF IFERROR IFNA FILTER COUNTIF COUNTIFS SUMIF SUMIFS AVERAGEIF AVERAGEIFS].freeze
11
+
12
+ def call(*args)
13
+ return ErrorValue.new(code: :value) unless arity_match?(args.length)
14
+ unless handles_errors?
15
+ error = args.lazy.map { |value| first_error(value) }.find(&:itself)
16
+ return error if error
17
+ end
18
+
19
+ implementation.call(*args)
20
+ rescue ZeroDivisionError
21
+ ErrorValue.new(code: :div0)
22
+ rescue Math::DomainError, RangeError
23
+ ErrorValue.new(code: :num)
24
+ rescue ArgumentError, TypeError, FloatDomainError
25
+ ErrorValue.new(code: :value)
26
+ end
27
+
28
+ def accepts?(count) = arity_match?(count)
29
+
30
+ private
31
+
32
+ def arity_match?(count)
33
+ case arity
34
+ when Integer then count == arity
35
+ when Range then arity.cover?(count)
36
+ when Array then arity.include?(count)
37
+ else true
38
+ end
39
+ end
40
+
41
+ def handles_errors?
42
+ ERROR_HANDLERS.include?(name) || name.start_with?("IS") || name == "ERROR.TYPE"
43
+ end
44
+
45
+ def first_error(value)
46
+ case value
47
+ when ErrorValue then value
48
+ when ArrayValue then value.rows.flatten.find { |cell| cell.is_a?(ErrorValue) }
49
+ when Array then value.flatten.find { |cell| cell.is_a?(ErrorValue) }
50
+ end
51
+ end
52
+ end
53
+
54
+ class Registry
55
+ include Enumerable
56
+
57
+ def initialize
58
+ @entries = {}
59
+ end
60
+
61
+ def initialize_copy(other)
62
+ super
63
+ @entries = other.instance_variable_get(:@entries).dup
64
+ end
65
+
66
+ def register(name, arity:, volatile: false, &implementation)
67
+ raise ArgumentError, "implementation block is required" unless implementation
68
+
69
+ name = name.to_s.upcase
70
+ raise ArgumentError, "invalid function name: #{name}" unless name.match?(/\A[A-Z][A-Z0-9_.]*\z/)
71
+ raise ArgumentError, "function already registered: #{name}" if @entries.key?(name)
72
+
73
+ @entries[name] = Entry.new(name: name, arity: arity, volatile: !!volatile,
74
+ implementation: implementation)
75
+ self
76
+ end
77
+
78
+ def [](name) = @entries[name.to_s.upcase]
79
+ def names = @entries.keys.sort.freeze
80
+ def each(&block) = @entries.values.each(&block)
81
+ def size = @entries.size
82
+
83
+ def call(name, *args)
84
+ entry = self[name]
85
+ return ErrorValue.new(code: :name) unless entry
86
+
87
+ entry.call(*args)
88
+ end
89
+ end
90
+
91
+ module_function
92
+
93
+ def standard
94
+ (@standard ||= build_standard).dup
95
+ end
96
+
97
+ def build_standard
98
+ registry = Registry.new
99
+ register_math(registry)
100
+ register_statistics(registry)
101
+ register_logic_and_text(registry)
102
+ register_dates(registry)
103
+ register_lookup_and_info(registry)
104
+ register_finance(registry)
105
+ registry
106
+ end
107
+
108
+ def register_math(registry)
109
+ unary = {
110
+ "ABS" => ->(x) { x.abs }, "ACOS" => Math.method(:acos), "ACOSH" => Math.method(:acosh),
111
+ "ASIN" => Math.method(:asin), "ASINH" => Math.method(:asinh), "ATAN" => Math.method(:atan),
112
+ "ATANH" => Math.method(:atanh), "COS" => Math.method(:cos), "COSH" => Math.method(:cosh),
113
+ "DEGREES" => ->(x) { x * 180 / Math::PI }, "EXP" => Math.method(:exp), "INT" => ->(x) { x.floor },
114
+ "LN" => Math.method(:log), "LOG10" => Math.method(:log10), "RADIANS" => ->(x) { x * Math::PI / 180 },
115
+ "SIGN" => ->(x) { x <=> 0 }, "SIN" => Math.method(:sin), "SINH" => Math.method(:sinh),
116
+ "SQRT" => Math.method(:sqrt), "TAN" => Math.method(:tan), "TANH" => Math.method(:tanh),
117
+ "EVEN" => ->(x) { round_multiple(x.abs.ceil, 2) * (x.negative? ? -1 : 1) },
118
+ "ODD" => ->(x) { n = x.abs.ceil; n.even? ? (n + 1) * (x.negative? ? -1 : 1) : n * (x.negative? ? -1 : 1) },
119
+ "FACT" => ->(x) { factorial(x) }, "FACTDOUBLE" => ->(x) { double_factorial(x) },
120
+ "SQRTPI" => ->(x) { Math.sqrt(Math::PI * x) }, "SEC" => ->(x) { 1 / Math.cos(x) },
121
+ "SECH" => ->(x) { 1 / Math.cosh(x) }, "CSC" => ->(x) { 1 / Math.sin(x) },
122
+ "CSCH" => ->(x) { 1 / Math.sinh(x) }, "COT" => ->(x) { 1 / Math.tan(x) },
123
+ "COTH" => ->(x) { 1 / Math.tanh(x) }
124
+ }
125
+ unary.each do |name, fn|
126
+ register(registry, name, 1) { |x| fn.call(number!(x)) }
127
+ end
128
+ register(registry, "TRUNC", 1..2) { |x, digits = 0| truncate_decimal(number!(x), integer!(digits)) }
129
+ register(registry, "PI", 0) { Math::PI }
130
+ register(registry, "RAND", 0, volatile: true) { rand }
131
+ register(registry, "RANDBETWEEN", 2, volatile: true) { |a, b| rand(integer!(a)..integer!(b)) }
132
+ register(registry, "POWER", 2) { |a, b| number!(a)**number!(b) }
133
+ register(registry, "MOD", 2) { |a, b| number!(a) % number!(b) }
134
+ register(registry, "QUOTIENT", 2) { |a, b| (number!(a) / number!(b)).truncate }
135
+ register(registry, "ATAN2", 2) { |x, y| Math.atan2(number!(y), number!(x)) }
136
+ register(registry, "LOG", 1..2) { |x, base = 10| Math.log(number!(x), number!(base)) }
137
+ register(registry, "ROUND", 1..2) { |x, n = 0| number!(x).round(integer!(n)) }
138
+ register(registry, "ROUNDDOWN", 1..2) { |x, n = 0| truncate_decimal(number!(x), integer!(n)) }
139
+ register(registry, "ROUNDUP", 1..2) { |x, n = 0| round_up(number!(x), integer!(n)) }
140
+ %w[FLOOR CEILING FLOOR.MATH CEILING.MATH ISO.CEILING].each do |name|
141
+ register(registry, name, 1..3) do |number, significance = 1, mode = 0|
142
+ floor_ceiling(number!(number), number!(significance), name.start_with?("CEILING", "ISO"), number!(mode))
143
+ end
144
+ end
145
+ register(registry, "MROUND", 2) do |x, multiple|
146
+ number = number!(x); step = number!(multiple)
147
+ if (number.negative? && step.positive?) || (number.positive? && step.negative?)
148
+ ErrorValue.new(code: :num)
149
+ else
150
+ round_multiple(number, step)
151
+ end
152
+ end
153
+ register(registry, "COMBIN", 2) { |n, k| choose(integer!(n), integer!(k)) }
154
+ register(registry, "COMBINA", 2) { |n, k| choose(integer!(n) + integer!(k) - 1, integer!(k)) }
155
+ register(registry, "MULTINOMIAL", 1..100) { |*xs| factorial(xs.sum { |x| integer!(x) }) / xs.reduce(1) { |p, x| p * factorial(integer!(x)) } }
156
+ register(registry, "GCD", 1..255) { |*xs| xs.map { |x| integer!(x).abs }.reduce(0, :gcd) }
157
+ register(registry, "LCM", 1..255) { |*xs| xs.map { |x| integer!(x).abs }.reduce(1) { |a, b| a.zero? || b.zero? ? 0 : a.lcm(b) } }
158
+ register(registry, "PRODUCT", 1..255) { |*xs| numeric_values(xs).reduce(1, :*) }
159
+ register(registry, "SUM", 0..255) { |*xs| numeric_values(xs).sum }
160
+ register(registry, "SUMSQ", 1..255) { |*xs| numeric_values(xs).sum { |x| x * x } }
161
+ register(registry, "SUBTOTAL", 2..255) do |function, *xs|
162
+ values = numeric_values(xs)
163
+ case integer!(function)
164
+ when 1, 101 then average(values)
165
+ when 2, 102 then xs.flatten.count { |x| numeric?(x) }
166
+ when 4, 104 then values.max || 0
167
+ when 5, 105 then values.min || 0
168
+ when 9, 109 then values.sum
169
+ else ErrorValue.new(code: :value)
170
+ end
171
+ end
172
+ end
173
+
174
+ def register_statistics(registry)
175
+ register(registry, "AVERAGE", 1..255) { |*xs| average(numeric_values(xs)) }
176
+ register(registry, "AVERAGEA", 1..255) { |*xs| vals = flatten(xs).map { |x| x.nil? ? 0 : (x == true ? 1 : x == false || x.is_a?(String) ? 0 : number!(x)) }; average(vals) }
177
+ register(registry, "COUNT", 1..255) { |*xs| flatten(xs).count { |x| numeric?(x) } }
178
+ register(registry, "COUNTA", 1..255) { |*xs| flatten(xs).count { |x| !x.nil? && x != "" } }
179
+ register(registry, "COUNTBLANK", 1) { |x| flatten([x]).count { |v| v.nil? || v == "" } }
180
+ register(registry, "MAX", 1..255) { |*xs| numeric_values(xs).max || 0 }
181
+ register(registry, "MIN", 1..255) { |*xs| numeric_values(xs).min || 0 }
182
+ register(registry, "MAXA", 1..255) { |*xs| comparable_values(xs).max || 0 }
183
+ register(registry, "MINA", 1..255) { |*xs| comparable_values(xs).min || 0 }
184
+ register(registry, "MEDIAN", 1..255) { |*xs| median(numeric_values(xs)) }
185
+ register(registry, "MODE.SNGL", 1..255) do |*xs|
186
+ values = numeric_values(xs)
187
+ frequencies = values.tally
188
+ max = frequencies.values.max
189
+ max && max > 1 ? frequencies.select { |_, count| count == max }.keys.min : ErrorValue.new(code: :na)
190
+ end
191
+ register(registry, "LARGE", 2) { |array, k| numeric_values([array]).sort.reverse.fetch(integer!(k) - 1) { ErrorValue.new(code: :num) } }
192
+ register(registry, "SMALL", 2) { |array, k| numeric_values([array]).sort.fetch(integer!(k) - 1) { ErrorValue.new(code: :num) } }
193
+ register(registry, "STDEV.S", 1..255) { |*xs| deviation(numeric_values(xs), sample: true) }
194
+ register(registry, "STDEV.P", 1..255) { |*xs| deviation(numeric_values(xs), sample: false) }
195
+ register(registry, "VAR.S", 1..255) do |*xs|
196
+ result = deviation(numeric_values(xs), sample: true)
197
+ result.is_a?(ErrorValue) ? result : result**2
198
+ end
199
+ register(registry, "VAR.P", 1..255) do |*xs|
200
+ result = deviation(numeric_values(xs), sample: false)
201
+ result.is_a?(ErrorValue) ? result : result**2
202
+ end
203
+ register(registry, "COUNTIF", 2) { |range, criteria| flatten([range]).count { |x| criterion_match?(x, criteria) } }
204
+ register(registry, "COUNTIFS", 2..255) do |*xs|
205
+ ranges = xs.each_slice(2).map { |range, criteria| [flatten([range]), criteria] }
206
+ if xs.length.odd?
207
+ ErrorValue.new(code: :value)
208
+ else
209
+ size = ranges.first.first.length
210
+ ranges.all? { |values, _| values.length == size } ? (0...size).count { |i| ranges.all? { |values, criteria| criterion_match?(values[i], criteria) } } : ErrorValue.new(code: :value)
211
+ end
212
+ end
213
+ register(registry, "SUMIF", 2..3) do |range, criteria, sum_range = range|
214
+ values = flatten([range]); sums = flatten([sum_range])
215
+ values.each_index.sum { |i| criterion_match?(values[i], criteria) ? number_or_zero(sums[i]) : 0 }
216
+ end
217
+ register(registry, "SUMIFS", 3..255) do |sum_range, *xs|
218
+ sum_rows = row_matrix(sum_range); ranges = xs.each_slice(2).map { |r, c| [row_matrix(r), c] }
219
+ if xs.length.odd?
220
+ ErrorValue.new(code: :value)
221
+ elsif ranges.any? { |rows, _| rows.map(&:length) != sum_rows.map(&:length) }
222
+ ErrorValue.new(code: :value)
223
+ else
224
+ sums = sum_rows.flatten
225
+ ranges = ranges.map { |rows, criteria| [rows.flatten, criteria] }
226
+ (0...sums.length).sum { |i| ranges.all? { |values, criteria| values[i] && criterion_match?(values[i], criteria) } ? number_or_zero(sums[i]) : 0 }
227
+ end
228
+ end
229
+ register(registry, "AVERAGEIF", 2..3) do |range, criteria, average_range = range|
230
+ pairs = flatten([range]).zip(flatten([average_range])).select { |value, _| criterion_match?(value, criteria) }
231
+ average(pairs.map { |_, value| number_or_zero(value) })
232
+ end
233
+ register(registry, "AVERAGEIFS", 3..255) do |average_range, *xs|
234
+ vals = flatten([average_range]); ranges = xs.each_slice(2).map { |r, c| [flatten([r]), c] }
235
+ if xs.length.odd?
236
+ ErrorValue.new(code: :value)
237
+ else
238
+ average(vals.each_index.filter_map { |i| vals[i] if ranges.all? { |r, c| r[i] && criterion_match?(r[i], c) } }.map { |x| number_or_zero(x) })
239
+ end
240
+ end
241
+ end
242
+
243
+ def register_logic_and_text(registry)
244
+ register(registry, "TRUE", 0) { true }
245
+ register(registry, "FALSE", 0) { false }
246
+ register(registry, "AND", 1..255) { |*xs| flatten(xs).all? { |x| truthy?(x) } }
247
+ register(registry, "OR", 1..255) { |*xs| flatten(xs).any? { |x| truthy?(x) } }
248
+ register(registry, "XOR", 1..255) { |*xs| flatten(xs).count { |x| truthy?(x) }.odd? }
249
+ register(registry, "NOT", 1) { |x| !truthy?(x) }
250
+ register(registry, "IF", 2..3) { |test, yes, no = false| truthy?(test) ? yes : no }
251
+ register(registry, "IFERROR", 2) { |x, fallback| x.is_a?(ErrorValue) ? fallback : x }
252
+ register(registry, "IFNA", 2) { |x, fallback| x.is_a?(ErrorValue) && x.code == :na ? fallback : x }
253
+ register(registry, "IFS", 2..255) do |*xs|
254
+ pair = xs.each_slice(2).find { |test, _| truthy?(test) }
255
+ pair ? pair[1] : ErrorValue.new(code: :na)
256
+ end
257
+ register(registry, "SWITCH", 3..255) do |value, *xs|
258
+ pairs = xs[0...-1].each_slice(2).to_a
259
+ (pairs.find { |match, _| compare(value, match).zero? }&.last) || (xs.length.odd? ? xs.last : ErrorValue.new(code: :na))
260
+ end
261
+ register(registry, "CONCAT", 1..255) { |*xs| flatten(xs).map { |x| text(x) }.join }
262
+ register(registry, "CONCATENATE", 1..255) { |*xs| xs.map { |x| text(x) }.join }
263
+ register(registry, "TEXTJOIN", 3..255) do |delimiter, ignore_empty, *xs|
264
+ flatten(xs).reject { |x| truthy?(ignore_empty) && (x.nil? || x == "") }.map { |x| text(x) }.join(text(delimiter))
265
+ end
266
+ register(registry, "EXACT", 2) { |a, b| text(a) == text(b) }
267
+ register(registry, "LEFT", 1..2) { |s, n = 1| text(s)[0, integer!(n)] || "" }
268
+ register(registry, "RIGHT", 1..2) { |s, n = 1| text(s)[-integer!(n), integer!(n)] || "" }
269
+ register(registry, "MID", 3) { |s, start, count| text(s)[[integer!(start) - 1, 0].max, integer!(count)] || "" }
270
+ register(registry, "LEN", 1) { |s| text(s).length }
271
+ register(registry, "LOWER", 1) { |s| text(s).downcase }
272
+ register(registry, "UPPER", 1) { |s| text(s).upcase }
273
+ register(registry, "PROPER", 1) { |s| text(s).downcase.gsub(/\b[a-z]/) { |c| c.upcase } }
274
+ register(registry, "TRIM", 1) { |s| text(s).strip.gsub(/[ \t]+/, " ") }
275
+ register(registry, "CLEAN", 1) { |s| text(s).delete("\x00-\x1F") }
276
+ register(registry, "REPT", 2) { |s, n| text(s) * [[integer!(n), 0].max, 32_767].min }
277
+ register(registry, "FIND", 2..3) { |find, within, start = 1| (text(within).index(text(find), [integer!(start) - 1, 0].max) || ErrorValue.new(code: :value)).then { |i| i.is_a?(Integer) ? i + 1 : i } }
278
+ register(registry, "SEARCH", 2..3) do |find, within, start = 1|
279
+ offset = [integer!(start) - 1, 0].max
280
+ source = wildcard_regex(text(find)).source[2...-2]
281
+ match = /#{source}/i.match(text(within), offset)
282
+ match ? match.begin(0) + 1 : ErrorValue.new(code: :value)
283
+ end
284
+ register(registry, "REPLACE", 4) { |old, start, count, new_text| text(old).dup.tap { |s| s[integer!(start) - 1, integer!(count)] = text(new_text) } }
285
+ register(registry, "SUBSTITUTE", 3..4) do |old, find, replacement, instance = nil|
286
+ s = text(old); from = text(find); to = text(replacement)
287
+ if instance
288
+ count = 0
289
+ s.gsub(Regexp.new(Regexp.escape(from))) { |match| count += 1; count == integer!(instance) ? to : match }
290
+ else
291
+ s.gsub(from, to)
292
+ end
293
+ end
294
+ register(registry, "TEXT", 2) { |x, pattern| Format.apply(x, Format.parse(text(pattern))).first }
295
+ register(registry, "VALUE", 1) { |s| number!(text(s).delete(",")) }
296
+ register(registry, "CHAR", 1) { |x| integer!(x).clamp(1, 255).chr(Encoding::ISO_8859_1).encode(Encoding::UTF_8) }
297
+ register(registry, "CODE", 1) { |x| text(x).ord }
298
+ register(registry, "UNICHAR", 1) { |x| [integer!(x)].pack("U") }
299
+ register(registry, "UNICODE", 1) { |x| text(x).ord }
300
+ end
301
+
302
+ def register_dates(registry)
303
+ register(registry, "DATE", 3) do |year, month, day|
304
+ y = integer!(year); y += 1900 if y.between?(0, 1899)
305
+ month_index = y * 12 + integer!(month) - 1
306
+ normalized_year, normalized_month = month_index.divmod(12)
307
+ Date.new(normalized_year, normalized_month + 1, 1) + integer!(day) - 1
308
+ end
309
+ register(registry, "DATEVALUE", 1) { |s| date_serial(Date.parse(text(s))) }
310
+ register(registry, "DAY", 1) { |x| date_value(x).day }
311
+ register(registry, "MONTH", 1) { |x| date_value(x).month }
312
+ register(registry, "YEAR", 1) { |x| date_value(x).year }
313
+ register(registry, "DAYS", 2) { |end_date, start_date| (date_value(end_date) - date_value(start_date)).to_i }
314
+ register(registry, "DAYS360", 2..3) do |start_date, end_date, method = false|
315
+ a = date_value(start_date); b = date_value(end_date)
316
+ d1 = truthy?(method) ? [a.day, 30].min : (a.day == 31 ? 30 : a.day)
317
+ d2 = truthy?(method) ? [b.day, 30].min : (b.day == 31 && d1 == 30 ? 30 : b.day)
318
+ (b.year - a.year) * 360 + (b.month - a.month) * 30 + d2 - d1
319
+ end
320
+ register(registry, "EDATE", 2) { |d, months| date_value(d) >> integer!(months) }
321
+ register(registry, "EOMONTH", 2) do |d, months|
322
+ target = date_value(d) >> integer!(months)
323
+ target.next_month - target.next_month.day
324
+ end
325
+ register(registry, "HOUR", 1) { |x| (serial_fraction(x) * 24).floor }
326
+ register(registry, "MINUTE", 1) { |x| (serial_fraction(x) * 1440).floor % 60 }
327
+ register(registry, "SECOND", 1) { |x| (serial_fraction(x) * 86_400).floor % 60 }
328
+ register(registry, "TIME", 3) { |h, m, s| ((integer!(h) * 3600 + integer!(m) * 60 + integer!(s)) % 86_400) / 86_400.0 }
329
+ register(registry, "TIMEVALUE", 1) { |s| t = Time.parse(text(s)); (t.hour * 3600 + t.min * 60 + t.sec) / 86_400.0 }
330
+ register(registry, "TODAY", 0, volatile: true) { Date.today }
331
+ register(registry, "NOW", 0, volatile: true) { Time.now }
332
+ register(registry, "WEEKDAY", 1..2) { |d, type = 1| weekday(date_value(d), integer!(type)) }
333
+ register(registry, "WEEKNUM", 1..2) do |d, type = 1|
334
+ week_start = case integer!(type)
335
+ when 1 then 0
336
+ when 2 then 1
337
+ else next ErrorValue.new(code: :num)
338
+ end
339
+ week_number(date_value(d), week_start)
340
+ end
341
+ register(registry, "ISOWEEKNUM", 1) { |d| date_value(d).cweek }
342
+ register(registry, "NETWORKDAYS", 2..3) do |start, finish, holidays = []|
343
+ holidays = flatten([holidays]).map { |d| date_value(d) }.to_set
344
+ a = date_value(start); b = date_value(finish); sign = a <= b ? 1 : -1
345
+ (([a, b].min)..([a, b].max)).count { |d| ![0, 6].include?(d.wday) && !holidays.include?(d) } * sign
346
+ end
347
+ register(registry, "WORKDAY", 2..3) do |start, days, holidays = []|
348
+ holidays = flatten([holidays]).map { |d| date_value(d) }.to_set
349
+ date = date_value(start); remaining = integer!(days).abs; direction = integer!(days).negative? ? -1 : 1
350
+ while remaining.positive?
351
+ date += direction
352
+ remaining -= 1 unless [0, 6].include?(date.wday) || holidays.include?(date)
353
+ end
354
+ date
355
+ end
356
+ register(registry, "YEARFRAC", 2..3) { |a, b, basis = 0| year_fraction(date_value(a), date_value(b), integer!(basis)) }
357
+ end
358
+
359
+ def register_lookup_and_info(registry)
360
+ register(registry, "CHOOSE", 2..255) { |index, *values| values.fetch(integer!(index) - 1) { ErrorValue.new(code: :value) } }
361
+ register(registry, "INDEX", 2..4) do |array, row, column = 1, _area = 1|
362
+ values = matrix(array); values.fetch(integer!(row) - 1) { [] }.fetch(integer!(column) - 1, ErrorValue.new(code: :ref))
363
+ end
364
+ register(registry, "MATCH", 2..3) do |lookup, array, match_type = 0|
365
+ values = flatten([array]); index = if integer!(match_type).zero?
366
+ values.index { |value| compare(value, lookup).zero? }
367
+ else
368
+ candidates = values.each_with_index.select { |value, _| integer!(match_type).positive? ? compare(value, lookup) <= 0 : compare(value, lookup) >= 0 }
369
+ candidates.last&.last
370
+ end
371
+ index ? index + 1 : ErrorValue.new(code: :na)
372
+ end
373
+ register(registry, "VLOOKUP", 3..4) { |lookup, table, column, approximate = false| lookup_table(lookup, table, integer!(column), vertical: true, approximate: truthy?(approximate)) }
374
+ register(registry, "HLOOKUP", 3..4) { |lookup, table, row, approximate = false| lookup_table(lookup, table, integer!(row), vertical: false, approximate: truthy?(approximate)) }
375
+ register(registry, "XLOOKUP", 3..6) do |lookup, lookups, results, not_found = ErrorValue.new(code: :na), match_mode = 0, search_mode = 1|
376
+ lookup_rows = row_matrix(lookups); result_rows = row_matrix(results)
377
+ rectangular = ->(rows) { !rows.empty? && rows.all? { |row| row.is_a?(Array) && row.length == rows.first.length } }
378
+ vector = rectangular.call(lookup_rows) && !lookup_rows.first.empty? &&
379
+ (lookup_rows.length == 1 || lookup_rows.all? { |row| row.length == 1 })
380
+ if !vector || !rectangular.call(result_rows) || result_rows.first.empty?
381
+ ErrorValue.new(code: :value)
382
+ elsif number!(match_mode) != 0 || number!(search_mode) != 1
383
+ ErrorValue.new(code: :value)
384
+ else
385
+ horizontal = lookup_rows.length == 1
386
+ lookup_values = horizontal ? lookup_rows.first : lookup_rows.map(&:first)
387
+ result_shape_matches = horizontal ? result_rows.all? { |row| row.length == lookup_values.length } : result_rows.length == lookup_values.length
388
+ next ErrorValue.new(code: :value) unless result_shape_matches
389
+
390
+ index = lookup_values.index { |value| compare(value, lookup).zero? }
391
+ if index
392
+ if horizontal
393
+ values = result_rows.map { |row| row[index] }
394
+ values.length == 1 ? values.first : ArrayValue.new(rows: values.map { |value| [value] })
395
+ else
396
+ values = result_rows[index]
397
+ values.length == 1 ? values.first : ArrayValue.new(rows: [values])
398
+ end
399
+ else
400
+ not_found
401
+ end
402
+ end
403
+ end
404
+ register(registry, "LOOKUP", 2..3) do |lookup, vector, result = vector|
405
+ v = flatten([vector]); r = flatten([result]); i = v.rindex { |x| compare(x, lookup) <= 0 }
406
+ i ? r[i] : ErrorValue.new(code: :na)
407
+ end
408
+ register(registry, "ADDRESS", 2..5) do |row, column, abs = 1, a1 = true, sheet = nil|
409
+ r = integer!(row); c = integer!(column); mode = integer!(abs)
410
+ if !r.positive? || !c.between?(1, 16_384) || !mode.between?(1, 4)
411
+ ErrorValue.new(code: :value)
412
+ else
413
+ cell = if truthy?(a1)
414
+ name = Formula.column_name(c)
415
+ "#{mode == 1 || mode == 3 ? "$#{name}" : name}#{mode == 1 || mode == 2 ? "$" : ""}#{r}"
416
+ else
417
+ row_reference = mode == 1 || mode == 2 ? "R#{r}" : "R[#{r}]"
418
+ column_reference = mode == 1 || mode == 3 ? "C#{c}" : "C[#{c}]"
419
+ row_reference + column_reference
420
+ end
421
+ sheet.nil? ? cell : "#{Formula.render_sheet(text(sheet))}!#{cell}"
422
+ end
423
+ end
424
+ register(registry, "ROW", 0..1) { |ref = nil| ref.is_a?(Reference) ? ref.row : 1 }
425
+ register(registry, "COLUMN", 0..1) { |ref = nil| ref.is_a?(Reference) ? ref.column : 1 }
426
+ register(registry, "ROWS", 1) { |x| matrix(x).length }
427
+ register(registry, "COLUMNS", 1) { |x| matrix(x).first&.length || 0 }
428
+ register(registry, "AREAS", 1) { |x| x.is_a?(ArrayValue) ? 1 : ErrorValue.new(code: :value) }
429
+ register(registry, "ISBLANK", 1) { |x| x.nil? }
430
+ register(registry, "ISERR", 1) { |x| x.is_a?(ErrorValue) && x.code != :na }
431
+ register(registry, "ISERROR", 1) { |x| x.is_a?(ErrorValue) }
432
+ register(registry, "ISNA", 1) { |x| x.is_a?(ErrorValue) && x.code == :na }
433
+ register(registry, "ISNUMBER", 1) { |x| numeric?(x) }
434
+ register(registry, "ISTEXT", 1) { |x| x.is_a?(String) }
435
+ register(registry, "ISNONTEXT", 1) { |x| !x.is_a?(String) }
436
+ register(registry, "ISLOGICAL", 1) { |x| x == true || x == false }
437
+ register(registry, "ISEVEN", 1) { |x| integer!(x).even? }
438
+ register(registry, "ISODD", 1) { |x| integer!(x).odd? }
439
+ register(registry, "ISFORMULA", 1) { |_x| false }
440
+ register(registry, "N", 1) { |x| x == true ? 1 : x == false || x.nil? || x.is_a?(String) ? 0 : x.is_a?(ErrorValue) ? x : number!(x) }
441
+ register(registry, "NA", 0) { ErrorValue.new(code: :na) }
442
+ register(registry, "TYPE", 1) { |x| x.is_a?(Numeric) ? 1 : x.is_a?(String) ? 2 : x == true || x == false ? 4 : x.is_a?(ErrorValue) ? 16 : 64 }
443
+ register(registry, "ERROR.TYPE", 1) do |x|
444
+ if x.is_a?(ErrorValue)
445
+ { div0: 2, value: 3, ref: 4, name: 5, num: 6, na: 7, spill: 9, calc: 14 }.fetch(x.code, 1)
446
+ else
447
+ ErrorValue.new(code: :na)
448
+ end
449
+ end
450
+ register(registry, "FORMULATEXT", 1) { |x| x.is_a?(String) && x.start_with?("=") ? x : ErrorValue.new(code: :na) }
451
+ register(registry, "HYPERLINK", 1..2) { |url, label = url| text(label) }
452
+ register(registry, "CELL", 1..2) { |_info, _ref = nil| ErrorValue.new(code: :value) }
453
+ register(registry, "ISREF", 1) { |x| x.is_a?(Reference) || x.is_a?(Area) }
454
+ register(registry, "INDIRECT", 1..2) { |_ref, _a1 = true| ErrorValue.new(code: :ref) }
455
+ register(registry, "OFFSET", 3..5, volatile: true) { |_ref, _rows, _columns, _height = 1, _width = 1| ErrorValue.new(code: :ref) }
456
+ register(registry, "FILTER", 2..3) do |array, include, *fallback|
457
+ rows = matrix(array)
458
+ mask = matrix(include)
459
+ rows = [rows] if !rows.empty? && !rows.first.is_a?(Array)
460
+ mask = [mask] if !mask.empty? && !mask.first.is_a?(Array)
461
+ invalid_matrix = [rows, mask].any? do |values|
462
+ values.empty? || !values.first.is_a?(Array) || values.first.empty? ||
463
+ values.any? { |row| !row.is_a?(Array) || row.length != values.first.length }
464
+ end
465
+
466
+ if invalid_matrix
467
+ ErrorValue.new(code: :value)
468
+ else
469
+ by_row = mask.length == rows.length && mask.first.length == 1
470
+ by_column = mask.length == 1 && mask.first.length == rows.first.length
471
+ unless by_row || by_column
472
+ ErrorValue.new(code: :value)
473
+ else
474
+ selectors = by_row ? mask.map(&:first) : mask.first
475
+ error = selectors.find { |value| value.is_a?(ErrorValue) }
476
+ if error
477
+ error
478
+ else
479
+ indices = selectors.each_index.select { |index| truthy?(selectors[index]) }
480
+ if indices.empty?
481
+ fallback.empty? ? ErrorValue.new(code: :calc) : fallback.first
482
+ else
483
+ selected = if by_row
484
+ indices.map { |index| rows[index] }
485
+ else
486
+ rows.map { |row| indices.map { |index| row[index] } }
487
+ end
488
+ ArrayValue.new(rows: selected)
489
+ end
490
+ end
491
+ end
492
+ end
493
+ end
494
+ register(registry, "TRANSPOSE", 1) { |x| ArrayValue.new(rows: matrix(x).transpose) }
495
+ register(registry, "SEQUENCE", 1..4) do |rows, columns = 1, start = 1, step = 1|
496
+ r = integer!(rows); c = integer!(columns)
497
+ raise RangeError if r <= 0 || c <= 0 || r * c > 1_000_000
498
+ ArrayValue.new(rows: Array.new(r) { |i| Array.new(c) { |j| number!(start) + (i * c + j) * number!(step) } })
499
+ end
500
+ register(registry, "SORT", 1..4) do |x, index = 1, order = 1, by_column = false|
501
+ rows = matrix(x)
502
+ columns = truthy?(by_column)
503
+ items = columns ? rows.transpose : rows
504
+ sorted = items.sort_by { |item| item[integer!(index) - 1] }
505
+ sorted.reverse! if integer!(order).negative?
506
+ ArrayValue.new(rows: columns ? sorted.transpose : sorted)
507
+ end
508
+ unique = lambda do |array, by_col = false, exactly_once = false|
509
+ rows = matrix(array)
510
+ rows = rows.map { |value| [value] } if array.is_a?(Array) && !array.first.is_a?(Array)
511
+ invalid_matrix = rows.empty? || !rows.first.is_a?(Array) || rows.first.empty? ||
512
+ rows.any? { |row| !row.is_a?(Array) || row.length != rows.first.length }
513
+ valid_flags = [true, false, 0, 1].include?(by_col) && [true, false, 0, 1].include?(exactly_once)
514
+
515
+ if invalid_matrix || !valid_flags
516
+ ErrorValue.new(code: :value)
517
+ else
518
+ columns = by_col == true || by_col == 1
519
+ only_once = exactly_once == true || exactly_once == 1
520
+ items = columns ? rows.transpose : rows
521
+ counts = items.tally
522
+ unique = items.uniq
523
+ unique.select! { |item| counts.fetch(item) == 1 } if only_once
524
+ unique.empty? ? ErrorValue.new(code: :calc) : ArrayValue.new(rows: columns ? unique.transpose : unique)
525
+ end
526
+ end
527
+ registry.register("UNIQUE", arity: 1..3, &unique)
528
+ end
529
+
530
+ def register_finance(registry)
531
+ register(registry, "FV", 3..5) do |rate, periods, payment, present = 0, timing = 0|
532
+ r = number!(rate); n = number!(periods); pmt = number!(payment); pv = number!(present); type = number!(timing)
533
+ r.zero? ? -(pv + pmt * n) : -(pv * (1 + r)**n + pmt * (1 + r * type) * ((1 + r)**n - 1) / r)
534
+ end
535
+ register(registry, "PV", 3..5) do |rate, periods, payment, future = 0, timing = 0|
536
+ r = number!(rate); n = number!(periods); pmt = number!(payment); fv = number!(future); type = number!(timing)
537
+ r.zero? ? -fv - pmt * n : -(fv + pmt * (1 + r * type) * ((1 + r)**n - 1) / r) / (1 + r)**n
538
+ end
539
+ register(registry, "PMT", 3..5) do |rate, periods, present, future = 0, timing = 0|
540
+ r = number!(rate); n = number!(periods); pv = number!(present); fv = number!(future); type = number!(timing)
541
+ r.zero? ? -(pv + fv) / n : -(r * (fv + pv * (1 + r)**n)) / ((1 + r * type) * ((1 + r)**n - 1))
542
+ end
543
+ register(registry, "NPER", 3..5) do |rate, payment, present, future = 0, timing = 0|
544
+ r = number!(rate); pmt = number!(payment); pv = number!(present); fv = number!(future); type = number!(timing)
545
+ r.zero? ? -(pv + fv) / pmt : Math.log((pmt * (1 + r * type) - fv * r) / (pv * r + pmt * (1 + r * type))) / Math.log(1 + r)
546
+ end
547
+ register(registry, "NPV", 2..255) do |rate, *values|
548
+ r = number!(rate); numeric_values(values).each_with_index.sum { |value, i| value / ((1 + r)**(i + 1)) }
549
+ end
550
+ register(registry, "IRR", 1..2) do |*args|
551
+ irr(numeric_values([args.fetch(0)]), number!(args.fetch(1, 0.1)))
552
+ end
553
+ register(registry, "MIRR", 3) { |values, finance, reinvestment| mirr(numeric_values([values]), number!(finance), number!(reinvestment)) }
554
+ register(registry, "RATE", 3..6) { |periods, payment, present, future = 0, timing = 0, guess = 0.1| rate(number!(periods), number!(payment), number!(present), number!(future), number!(timing), number!(guess)) }
555
+ register(registry, "SLN", 3) { |cost, salvage, life| (number!(cost) - number!(salvage)) / number!(life) }
556
+ register(registry, "SYD", 4) { |cost, salvage, life, period| 2 * (number!(cost) - number!(salvage)) * (number!(life) - number!(period) + 1) / (number!(life) * (number!(life) + 1)) }
557
+ register(registry, "DDB", 4..5) { |cost, salvage, life, period, factor = 2| ddb(number!(cost), number!(salvage), number!(life), number!(period), number!(factor)) }
558
+ register(registry, "DB", 4..5) { |cost, salvage, life, period, month = 12| db(number!(cost), number!(salvage), number!(life), integer!(period), integer!(month)) }
559
+ register(registry, "EFFECT", 2) { |nominal, periods| (1 + number!(nominal) / integer!(periods))**integer!(periods) - 1 }
560
+ register(registry, "NOMINAL", 2) { |effective, periods| integer!(periods) * ((1 + number!(effective))**(1 / integer!(periods)) - 1) }
561
+ end
562
+
563
+ def register(registry, name, arity, volatile: false, &block)
564
+ registry.register(name, arity: arity, volatile: volatile, &block)
565
+ end
566
+
567
+ def flatten(values)
568
+ values.flat_map do |value|
569
+ case value
570
+ when ArrayValue then value.rows.flatten
571
+ when Array then value.flatten
572
+ else [value]
573
+ end
574
+ end
575
+ end
576
+
577
+ def numeric_values(values)
578
+ flatten(values).filter_map do |value|
579
+ case value
580
+ when Numeric then value
581
+ when Date then date_serial(value)
582
+ when Time then date_serial(value.to_date) + serial_fraction(value)
583
+ when true then 1
584
+ when false, nil, String, ErrorValue then nil
585
+ else nil
586
+ end
587
+ end
588
+ end
589
+
590
+ def number!(value)
591
+ return value if value.is_a?(Numeric)
592
+ return 1 if value == true
593
+ return 0 if value == false || value.nil?
594
+ return date_serial(value) if value.is_a?(Date)
595
+ return Float(value) if value.is_a?(String) && value.strip.match?(/\A[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?\z/i)
596
+
597
+ raise ArgumentError, "not numeric"
598
+ end
599
+
600
+ def integer!(value) = number!(value).to_i
601
+ def numeric?(value) = value.is_a?(Numeric) || value.is_a?(Date) || value.is_a?(Time)
602
+ def number_or_zero(value) = value.nil? ? 0 : (value.is_a?(ErrorValue) ? 0 : number!(value))
603
+ def text(value) = value.nil? ? "" : value == true ? "TRUE" : value == false ? "FALSE" : value.to_s
604
+ def truthy?(value) = !(value.nil? || value == false || value == 0 || value == "")
605
+ def average(values) = values.empty? ? ErrorValue.new(code: :div0) : values.sum.to_f / values.length
606
+ def median(values) = values.empty? ? ErrorValue.new(code: :num) : values.sort.then { |a| a.length.odd? ? a[a.length / 2] : (a[a.length / 2 - 1] + a[a.length / 2]) / 2.0 }
607
+
608
+ def deviation(values, sample:)
609
+ denominator = values.length - (sample ? 1 : 0)
610
+ return ErrorValue.new(code: sample && denominator < 1 ? :div0 : :num) unless denominator.positive?
611
+
612
+ mean = values.sum.to_f / values.length
613
+ Math.sqrt(values.sum { |x| (x - mean)**2 } / denominator)
614
+ end
615
+
616
+ def factorial(value)
617
+ n = Integer(value)
618
+ raise RangeError if n.negative? || n > 170
619
+ (1..n).reduce(1, :*)
620
+ end
621
+
622
+ def double_factorial(value)
623
+ n = Integer(value)
624
+ raise RangeError if n.negative? || n > 300
625
+ (1..n).select { |x| (x - n).even? }.reduce(1, :*)
626
+ end
627
+
628
+ def choose(n, k)
629
+ return 0 if k.negative? || k > n
630
+ return 0 if n.negative? || n > 10_000
631
+
632
+ k = [k, n - k].min
633
+ (1..k).reduce(1) { |value, i| value * (n - k + i) / i }
634
+ end
635
+
636
+ def round_multiple(value, multiple)
637
+ raise ZeroDivisionError if multiple.zero?
638
+ (value.fdiv(multiple).round) * multiple
639
+ end
640
+
641
+ def truncate_decimal(value, digits)
642
+ factor = 10.0**digits
643
+ (value * factor).truncate / factor
644
+ end
645
+
646
+ def round_up(value, digits)
647
+ factor = 10.0**digits
648
+ ((value * factor).abs.ceil * (value.negative? ? -1 : 1)) / factor
649
+ end
650
+
651
+ def floor_ceiling(value, significance, ceiling, mode)
652
+ raise ZeroDivisionError if significance.zero?
653
+ return 0 if value.zero?
654
+ quotient = value.fdiv(significance)
655
+ if ceiling
656
+ (value.negative? && mode != 0 ? quotient.floor : quotient.ceil) * significance
657
+ else
658
+ (value.negative? && mode != 0 ? quotient.ceil : quotient.floor) * significance
659
+ end
660
+ end
661
+
662
+ def comparable_values(values)
663
+ flatten(values).filter_map { |x| x == true ? 1 : x == false || x.nil? || x.is_a?(String) ? 0 : (number!(x) rescue nil) }
664
+ end
665
+
666
+ def criterion_match?(value, criteria)
667
+ return false if value.is_a?(ErrorValue)
668
+ criterion = text(criteria)
669
+ operator, target = criterion.match(/\A(<=|>=|<>|=|<|>)(.*)\z/)&.captures || ["=", criterion]
670
+ if %w[< <= > >= <>].include?(operator)
671
+ left = value.is_a?(Numeric) ? value : text(value).downcase
672
+ right = target.match?(/\A[+-]?(?:\d+(?:\.\d*)?|\.\d+)\z/) ? target.to_f : target.downcase
673
+ comparison = left <=> right
674
+ return false unless comparison
675
+ return { "<" => comparison.negative?, "<=" => !comparison.positive?, ">" => comparison.positive?, ">=" => !comparison.negative?, "<>" => !comparison.zero? }.fetch(operator)
676
+ end
677
+ return value.nil? || value == "" if target == ""
678
+ regex = wildcard_regex(target)
679
+ text(value).casecmp?(target) || text(value).match?(regex)
680
+ end
681
+
682
+ def wildcard_regex(pattern)
683
+ source = Regexp.escape(pattern).gsub("~\\*", "\\*").gsub("~\\?", "\\?").gsub("\\*", ".*").gsub("\\?", ".")
684
+ /\A#{source}\z/i
685
+ end
686
+
687
+ def compare(left, right)
688
+ left = left.nil? ? 0 : left
689
+ right = right.nil? ? 0 : right
690
+ if left.is_a?(Numeric) && right.is_a?(Numeric)
691
+ left <=> right
692
+ else
693
+ text(left).downcase <=> text(right).downcase
694
+ end
695
+ end
696
+
697
+ def matrix(value)
698
+ value.is_a?(ArrayValue) ? value.rows : value.is_a?(Array) ? value : [[value]]
699
+ end
700
+
701
+ def row_matrix(value)
702
+ rows = matrix(value)
703
+ rows.empty? || !rows.first.is_a?(Array) ? [rows] : rows
704
+ end
705
+
706
+ def lookup_table(lookup, table, index, vertical:, approximate: false)
707
+ rows = matrix(table)
708
+ return ErrorValue.new(code: :ref) unless index.positive?
709
+ lookup_values = vertical ? rows.map(&:first) : rows.first
710
+ result_values = vertical ? rows.map { |row| row[index - 1] } : (rows[index - 1] || [])
711
+ found = lookup_values.index { |value| compare(value, lookup).zero? }
712
+ found ||= lookup_values.each_index.select { |i| compare(lookup_values[i], lookup) <= 0 }.last if approximate
713
+ found ? result_values[found] : ErrorValue.new(code: :na)
714
+ end
715
+
716
+ def date_serial(value) = (value.to_date - Date.new(1899, 12, 30)).to_i
717
+ def date_value(value) = value.is_a?(Date) ? value : Date.new(1899, 12, 30) + number!(value).to_i
718
+ def serial_fraction(value) = value.is_a?(Time) ? (value.hour * 3600 + value.min * 60 + value.sec + value.nsec / 1e9) / 86_400.0 : number!(value) % 1
719
+ def week_number(date, week_start)
720
+ first_day = Date.new(date.year, 1, 1).wday
721
+ (date.yday - 1 + (first_day - week_start) % 7) / 7 + 1
722
+ end
723
+ def weekday(date, type) = type == 2 ? ((date.wday + 6) % 7) + 1 : type == 3 ? (date.wday + 6) % 7 : date.wday + 1
724
+ def year_fraction(a, b, basis) = basis == 1 ? (b - a).to_i / (a.leap? ? 366.0 : 365.0) : (b.year - a.year) + (b.yday - a.yday) / 365.0
725
+
726
+ def irr(values, guess = 0.1)
727
+ 100.times do
728
+ value = values.each_with_index.sum { |cash, i| cash / (1 + guess)**i }
729
+ derivative = values.each_with_index.sum { |cash, i| i.zero? ? 0 : -i * cash / (1 + guess)**(i + 1) }
730
+ return guess if value.abs < 1e-9
731
+ return ErrorValue.new(code: :num) if derivative.zero?
732
+
733
+ guess -= value / derivative
734
+ end
735
+ ErrorValue.new(code: :num)
736
+ end
737
+
738
+ def mirr(values, finance, reinvestment)
739
+ n = values.length
740
+ positive = values.each_with_index.sum { |x, i| x.positive? ? x * (1 + reinvestment)**(n - i - 1) : 0 }
741
+ negative = values.each_with_index.sum { |x, i| x.negative? ? x / (1 + finance)**i : 0 }
742
+ (positive / -negative)**(1.0 / (n - 1)) - 1
743
+ end
744
+
745
+ def rate(periods, payment, present, future, timing, guess)
746
+ x = guess
747
+ 100.times do
748
+ f = x.zero? ? present + payment * periods + future : present * (1 + x)**periods + payment * (1 + x * timing) * ((1 + x)**periods - 1) / x + future
749
+ return x if f.abs < 1e-9
750
+ derivative = (rate_equation(periods, payment, present, future, timing, x + 1e-6) - f) / 1e-6
751
+ return ErrorValue.new(code: :num) if derivative.zero?
752
+
753
+ x -= f / derivative
754
+ end
755
+ ErrorValue.new(code: :num)
756
+ end
757
+
758
+ def rate_equation(n, pmt, pv, fv, type, rate)
759
+ pv * (1 + rate)**n + pmt * (1 + rate * type) * ((1 + rate)**n - 1) / rate + fv
760
+ end
761
+
762
+ def db(cost, salvage, life, period, month)
763
+ raise RangeError unless cost.positive? && salvage >= 0 && salvage < cost && life.positive? && period.positive? && month.between?(1, 12)
764
+ raise RangeError if period > life.ceil + (month < 12 ? 1 : 0)
765
+
766
+ rate = (1 - (salvage / cost.to_f)**(1.0 / life)).round(3)
767
+ book_value = cost
768
+ depreciation = 0.0
769
+ 1.upto(period) do |current|
770
+ depreciation = if current == 1
771
+ cost * rate * month / 12.0
772
+ elsif month < 12 && current == life.ceil + 1
773
+ book_value * rate * (12 - month) / 12.0
774
+ else
775
+ book_value * rate
776
+ end
777
+ book_value -= depreciation
778
+ end
779
+ depreciation
780
+ end
781
+
782
+ def ddb(cost, salvage, life, period, factor)
783
+ raise RangeError unless cost >= 0 && salvage >= 0 && life.positive? && period.positive? && factor.positive?
784
+
785
+ book_value = cost
786
+ depreciation = 0.0
787
+ 1.upto(integer!(period)) do
788
+ depreciation = [book_value * factor / life.to_f, book_value - salvage].min
789
+ book_value -= depreciation
790
+ end
791
+ depreciation
792
+ end
793
+ end
794
+ end