zaxcel 0.1.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 +7 -0
- data/.rspec +4 -0
- data/.rubocop.yml +9 -0
- data/CHANGELOG.md +29 -0
- data/CONTRIBUTING.md +110 -0
- data/LICENSE +22 -0
- data/QUICK_START.md +187 -0
- data/README.md +372 -0
- data/Rakefile +18 -0
- data/SETUP.md +178 -0
- data/lib/enumerable.rb +47 -0
- data/lib/zaxcel/README.md +37 -0
- data/lib/zaxcel/arithmetic.rb +88 -0
- data/lib/zaxcel/binary_expression.rb +74 -0
- data/lib/zaxcel/binary_expressions/addition.rb +36 -0
- data/lib/zaxcel/binary_expressions/division.rb +24 -0
- data/lib/zaxcel/binary_expressions/multiplication.rb +24 -0
- data/lib/zaxcel/binary_expressions/subtraction.rb +41 -0
- data/lib/zaxcel/binary_expressions.rb +38 -0
- data/lib/zaxcel/cell.rb +141 -0
- data/lib/zaxcel/cell_formula.rb +16 -0
- data/lib/zaxcel/column.rb +142 -0
- data/lib/zaxcel/document.rb +136 -0
- data/lib/zaxcel/function.rb +6 -0
- data/lib/zaxcel/functions/abs.rb +18 -0
- data/lib/zaxcel/functions/and.rb +23 -0
- data/lib/zaxcel/functions/average.rb +17 -0
- data/lib/zaxcel/functions/choose.rb +20 -0
- data/lib/zaxcel/functions/concatenate.rb +20 -0
- data/lib/zaxcel/functions/if.rb +38 -0
- data/lib/zaxcel/functions/if_error.rb +25 -0
- data/lib/zaxcel/functions/index.rb +20 -0
- data/lib/zaxcel/functions/len.rb +16 -0
- data/lib/zaxcel/functions/match/match_type.rb +13 -0
- data/lib/zaxcel/functions/match.rb +27 -0
- data/lib/zaxcel/functions/max.rb +17 -0
- data/lib/zaxcel/functions/min.rb +17 -0
- data/lib/zaxcel/functions/negate.rb +26 -0
- data/lib/zaxcel/functions/or.rb +23 -0
- data/lib/zaxcel/functions/round.rb +20 -0
- data/lib/zaxcel/functions/sum.rb +18 -0
- data/lib/zaxcel/functions/sum_if.rb +20 -0
- data/lib/zaxcel/functions/sum_ifs.rb +34 -0
- data/lib/zaxcel/functions/sum_product.rb +18 -0
- data/lib/zaxcel/functions/text.rb +17 -0
- data/lib/zaxcel/functions/unique.rb +23 -0
- data/lib/zaxcel/functions/x_lookup.rb +28 -0
- data/lib/zaxcel/functions/xirr.rb +27 -0
- data/lib/zaxcel/functions.rb +169 -0
- data/lib/zaxcel/if_builder.rb +22 -0
- data/lib/zaxcel/lang.rb +23 -0
- data/lib/zaxcel/reference.rb +28 -0
- data/lib/zaxcel/references/cell.rb +42 -0
- data/lib/zaxcel/references/column.rb +49 -0
- data/lib/zaxcel/references/range.rb +35 -0
- data/lib/zaxcel/references/row.rb +34 -0
- data/lib/zaxcel/references.rb +5 -0
- data/lib/zaxcel/roundable.rb +14 -0
- data/lib/zaxcel/row.rb +93 -0
- data/lib/zaxcel/sheet.rb +425 -0
- data/lib/zaxcel/sorbet/enumerizable_enum.rb +50 -0
- data/lib/zaxcel/version.rb +6 -0
- data/lib/zaxcel.rb +73 -0
- data/zaxcel.gemspec +73 -0
- metadata +266 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Document
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
MAX_SHEET_NAME_LENGTH = 31
|
|
8
|
+
DEFAULT_WIDTH_UNITS_BY_DEFAULT_CHARACTER = 0.85 # empirically determined for 11pt body Calibri
|
|
9
|
+
|
|
10
|
+
sig { returns(Float) }
|
|
11
|
+
attr_reader :width_units_by_default_character
|
|
12
|
+
|
|
13
|
+
sig { params(width_units_by_default_character: Float).void }
|
|
14
|
+
def initialize(width_units_by_default_character: DEFAULT_WIDTH_UNITS_BY_DEFAULT_CHARACTER)
|
|
15
|
+
@document = T.let(Axlsx::Package.new, Axlsx::Package)
|
|
16
|
+
@document.workbook.escape_formulas = false
|
|
17
|
+
@width_units_by_default_character = width_units_by_default_character
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig { params(sheet: Axlsx::Worksheet, range_to_include: String).void }
|
|
21
|
+
def set_print_area(sheet:, range_to_include:)
|
|
22
|
+
@document.workbook.add_defined_name(range_to_include.to_s, local_sheet_id: sheet.index, name: '_xlnm.Print_Area')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig { params(sheet_name: String, sheet_visibility: Zaxcel::Sheet::SheetVisibility).returns(Zaxcel::Sheet) }
|
|
26
|
+
def add_sheet!(sheet_name, sheet_visibility: Zaxcel::Sheet::SheetVisibility::Visible)
|
|
27
|
+
clean_sheet_name = clean_sheet_name(sheet_name)
|
|
28
|
+
|
|
29
|
+
worksheet = @document.workbook.add_worksheet({ name: clean_sheet_name })
|
|
30
|
+
worksheet.page_setup.fit_to(width: 1, height: 1)
|
|
31
|
+
|
|
32
|
+
# This is necessary to show grouped columns, etc. The default value is false, which is inconsistent with new
|
|
33
|
+
# new documents created in excel. See https://github.com/caxlsx/caxlsx/issues/344 for additional context.
|
|
34
|
+
worksheet.sheet_view.show_outline_symbols = true
|
|
35
|
+
|
|
36
|
+
sheet = Zaxcel::Sheet.new(name: sheet_name, document: self, worksheet: worksheet, visibility: sheet_visibility)
|
|
37
|
+
sheet_by_name[sheet_name] = sheet
|
|
38
|
+
|
|
39
|
+
sheet
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { params(name: Symbol).returns(T.nilable(Integer)) }
|
|
43
|
+
def style(name)
|
|
44
|
+
styles_by_style_name[name]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
sig { params(name: Symbol, kwargs: T.untyped).returns(Integer) }
|
|
48
|
+
def add_style!(name, **kwargs)
|
|
49
|
+
styles_by_style_name[name] ||= @document.workbook.styles.add_style(**kwargs)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { returns(T::Hash[Symbol, Integer]) }
|
|
53
|
+
def styles_by_style_name
|
|
54
|
+
@styles ||= T.let({}, T.nilable(T::Hash[Symbol, Integer]))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
sig { params(name: String).returns(T.nilable(Zaxcel::Sheet)) }
|
|
58
|
+
def sheet(name)
|
|
59
|
+
sheet_by_name[name]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig { returns(T::Hash[String, Zaxcel::Sheet]) }
|
|
63
|
+
def sheet_by_name
|
|
64
|
+
@sheet_by_name ||= T.let({}, T.nilable(T::Hash[String, Zaxcel::Sheet]))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sig { returns(T.nilable(String)) }
|
|
68
|
+
def file_contents
|
|
69
|
+
stream = T.cast(@document.to_stream, T.any(StringIO, T::Boolean))
|
|
70
|
+
return if !stream.is_a?(StringIO)
|
|
71
|
+
|
|
72
|
+
stream.read
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# this exposes the underlying axlsx document directly. it's an escape hatch for modifying the document in ways not
|
|
76
|
+
# directly supported by zaxcel. it is unsafe in that it can break zaxcel's assumptions about the document leading to
|
|
77
|
+
# unintended consequences. use with caution.
|
|
78
|
+
sig { returns(Axlsx::Package) }
|
|
79
|
+
def unsafe_axlsx_document
|
|
80
|
+
@document
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
sig { params(sheet_name: String, pending_sheet_names: T::Array[String]).returns(String) }
|
|
84
|
+
def clean_sheet_name(sheet_name, pending_sheet_names: [])
|
|
85
|
+
# sheet names in excel must
|
|
86
|
+
# 1. be max 31 charaters
|
|
87
|
+
# 2. not special characters
|
|
88
|
+
# 3. be unique
|
|
89
|
+
cleaned_sheet_name = sheet_name.gsub(/[^0-9a-z_ ]/i, '').parameterize(
|
|
90
|
+
separator: ' ',
|
|
91
|
+
preserve_case: true,
|
|
92
|
+
).first(MAX_SHEET_NAME_LENGTH)
|
|
93
|
+
|
|
94
|
+
unique_index = 0
|
|
95
|
+
|
|
96
|
+
unique_name = cleaned_sheet_name
|
|
97
|
+
while @document.workbook.sheet_by_name(unique_name).present? || pending_sheet_names.include?(unique_name)
|
|
98
|
+
unique_index += 1
|
|
99
|
+
unique_name = "#{cleaned_sheet_name.first(cleaned_sheet_name.length - unique_index.to_s.length)}#{unique_index}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
unique_name
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class << self
|
|
106
|
+
extend T::Sig
|
|
107
|
+
|
|
108
|
+
sig { params(start_reference: Zaxcel::References::Cell, end_reference: Zaxcel::References::Cell).returns(T::Array[Zaxcel::Cell]) }
|
|
109
|
+
def cells_in_range(start_reference:, end_reference:)
|
|
110
|
+
cell_start = T.must(start_reference.cell)
|
|
111
|
+
cell_end = T.must(end_reference.cell)
|
|
112
|
+
|
|
113
|
+
if cell_start.column == cell_end.column
|
|
114
|
+
column = cell_start.column
|
|
115
|
+
|
|
116
|
+
cells_in_column = column.cell_by_row_name.values
|
|
117
|
+
cells_in_column.select do |cell|
|
|
118
|
+
row_position = T.must(cell.row_position)
|
|
119
|
+
|
|
120
|
+
row_position >= T.must(cell_start.row_position) && row_position <= T.must(cell_end.row_position)
|
|
121
|
+
end
|
|
122
|
+
elsif cell_start.row == cell_end.row
|
|
123
|
+
row = cell_start.row
|
|
124
|
+
|
|
125
|
+
cells_in_row = row.cell_by_column_name.values
|
|
126
|
+
cells_in_row.select do |cell|
|
|
127
|
+
col_position = T.must(cell.col_position)
|
|
128
|
+
|
|
129
|
+
col_position >= T.must(cell_start.col_position) && col_position <= T.must(cell_end.col_position)
|
|
130
|
+
end
|
|
131
|
+
else
|
|
132
|
+
raise 'Cells must be in the same row or column in order to sum'
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
# Functions don't have any special interface beyond cell formulas, but it's useful to be able to detect them since they
|
|
5
|
+
# interact with other cell formulas in their own way.
|
|
6
|
+
class Zaxcel::Function < Zaxcel::CellFormula; end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::Abs < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig { params(value: Zaxcel::Cell::ValueType).void }
|
|
8
|
+
def initialize(value)
|
|
9
|
+
@value = value
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
13
|
+
def format(on_sheet:)
|
|
14
|
+
formatted_value = Zaxcel::Cell.format(@value, on_sheet: on_sheet) || 0
|
|
15
|
+
|
|
16
|
+
"ABS(#{formatted_value})"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::And < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
# todo - likely and can accept more than one arg, but not sure. check into this.
|
|
8
|
+
# also, it may accept only specific types of formulas (returns true or false or something), but my suspicion is that
|
|
9
|
+
# is not true. confirm this also, since right now it's typed to a strange type.
|
|
10
|
+
sig { params(left_hand_value: Zaxcel::Cell::ValueType, right_hand_value: Zaxcel::Cell::ValueType).void }
|
|
11
|
+
def initialize(left_hand_value, right_hand_value)
|
|
12
|
+
@left_hand_value = left_hand_value
|
|
13
|
+
@right_hand_value = right_hand_value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
17
|
+
def format(on_sheet:)
|
|
18
|
+
left_hand_formatted_value = Zaxcel::Cell.format(@left_hand_value, on_sheet: on_sheet)
|
|
19
|
+
right_hand_formatted_value = Zaxcel::Cell.format(@right_hand_value, on_sheet: on_sheet)
|
|
20
|
+
|
|
21
|
+
"AND(#{left_hand_formatted_value},#{right_hand_formatted_value})"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::Average < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
# todo - match this to the excel signature if it takes an unbounded argument array
|
|
8
|
+
sig { params(range: Zaxcel::References::Range).void }
|
|
9
|
+
def initialize(range)
|
|
10
|
+
@range = range
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
14
|
+
def format(on_sheet:)
|
|
15
|
+
"AVERAGE(#{@range.format(on_sheet: on_sheet)})"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::Choose < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig { params(choice_index: Zaxcel::Cell::ValueType, choices: T::Array[Zaxcel::Cell::ValueType]).void }
|
|
8
|
+
def initialize(choice_index, choices:)
|
|
9
|
+
@choice_index = choice_index
|
|
10
|
+
@choices = choices
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
14
|
+
def format(on_sheet:)
|
|
15
|
+
args = [Zaxcel::Cell.format(@choice_index, on_sheet: on_sheet)]
|
|
16
|
+
args += @choices.map { |choice| Zaxcel::Cell.format(choice, on_sheet: on_sheet) }
|
|
17
|
+
|
|
18
|
+
"CHOOSE(#{args.join(',')})"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::Concatenate < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig { params(values: T::Array[Zaxcel::Cell::ValueType]).void }
|
|
8
|
+
def initialize(values)
|
|
9
|
+
@values = values
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
13
|
+
def format(on_sheet:)
|
|
14
|
+
formatted_values = @values.map do |value|
|
|
15
|
+
Zaxcel::Cell.format(value, on_sheet: on_sheet)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
"CONCATENATE(#{formatted_values.join(',')})"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::If < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
# access to 'clauses' can be helpful if debugging
|
|
8
|
+
sig { returns(Zaxcel::Cell::ValueType) }
|
|
9
|
+
attr_reader :condition
|
|
10
|
+
|
|
11
|
+
sig { returns(Zaxcel::Cell::ValueType) }
|
|
12
|
+
attr_reader :if_true
|
|
13
|
+
|
|
14
|
+
sig { returns(Zaxcel::Cell::ValueType) }
|
|
15
|
+
attr_reader :if_false
|
|
16
|
+
|
|
17
|
+
sig do
|
|
18
|
+
params(
|
|
19
|
+
condition: Zaxcel::Cell::ValueType,
|
|
20
|
+
if_true: Zaxcel::Cell::ValueType,
|
|
21
|
+
if_false: Zaxcel::Cell::ValueType,
|
|
22
|
+
).void
|
|
23
|
+
end
|
|
24
|
+
def initialize(condition, if_true:, if_false:)
|
|
25
|
+
@condition = condition
|
|
26
|
+
@if_true = if_true
|
|
27
|
+
@if_false = if_false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
31
|
+
def format(on_sheet:)
|
|
32
|
+
formatted_condition = Zaxcel::Cell.format(@condition, on_sheet: on_sheet)
|
|
33
|
+
formatted_if_true = Zaxcel::Cell.format(@if_true, on_sheet: on_sheet)
|
|
34
|
+
formatted_if_false = Zaxcel::Cell.format(@if_false, on_sheet: on_sheet)
|
|
35
|
+
|
|
36
|
+
"IF(#{formatted_condition},#{formatted_if_true},#{formatted_if_false})"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::IfError < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig do
|
|
8
|
+
params(
|
|
9
|
+
value: Zaxcel::Cell::ValueType,
|
|
10
|
+
default_value: Zaxcel::Cell::ValueType,
|
|
11
|
+
).void
|
|
12
|
+
end
|
|
13
|
+
def initialize(value, default_value:)
|
|
14
|
+
@value = value
|
|
15
|
+
@default_value = default_value
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
19
|
+
def format(on_sheet:)
|
|
20
|
+
formatted_value = Zaxcel::Cell.format(@value, on_sheet: on_sheet)
|
|
21
|
+
formatted_default = Zaxcel::Cell.format(@default_value, on_sheet: on_sheet)
|
|
22
|
+
|
|
23
|
+
"IFERROR(#{formatted_value},#{formatted_default})"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::Index < Zaxcel::Function
|
|
5
|
+
sig do
|
|
6
|
+
params(
|
|
7
|
+
index_value: Zaxcel::Cell::ValueType,
|
|
8
|
+
range: Zaxcel::References::Range,
|
|
9
|
+
).void
|
|
10
|
+
end
|
|
11
|
+
def initialize(index_value:, range:)
|
|
12
|
+
@index_value = index_value
|
|
13
|
+
@range = range
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
17
|
+
def format(on_sheet:)
|
|
18
|
+
"INDEX(#{@range.format(on_sheet: on_sheet)},#{Zaxcel::Cell.format(@index_value, on_sheet: on_sheet)})"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::Len < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig { params(value: Zaxcel::Cell::ValueType).void }
|
|
8
|
+
def initialize(value)
|
|
9
|
+
@value = value
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
13
|
+
def format(on_sheet:)
|
|
14
|
+
"LEN(#{Zaxcel::Cell.format(@value, on_sheet: on_sheet)})"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::Match::MatchType < T::Enum
|
|
5
|
+
enums do
|
|
6
|
+
# Finds the first value exactly equal to lookup_value. lookup_array can be in any order.
|
|
7
|
+
EXACT = new('0')
|
|
8
|
+
# Finds the largest value less than or equal to lookup_value. Requires lookup_array to be in ascending order.
|
|
9
|
+
LESS_THAN_OR_EQUAL = new('1')
|
|
10
|
+
# Finds the smallest value greater than or equal to lookup_value. Requires lookup_array to be in descending order.
|
|
11
|
+
GREATER_THAN_OR_EQUAL = new('-1')
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::Match < Zaxcel::Function
|
|
5
|
+
sig do
|
|
6
|
+
params(
|
|
7
|
+
value: Zaxcel::Cell::ValueType,
|
|
8
|
+
range: Zaxcel::References::Range,
|
|
9
|
+
match_type: T.nilable(Zaxcel::Functions::Match::MatchType),
|
|
10
|
+
).void
|
|
11
|
+
end
|
|
12
|
+
def initialize(value:, range:, match_type: nil)
|
|
13
|
+
@value = value
|
|
14
|
+
@range = range
|
|
15
|
+
@match_type = match_type
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
19
|
+
def format(on_sheet:)
|
|
20
|
+
args = [
|
|
21
|
+
Zaxcel::Cell.format(@value, on_sheet: on_sheet),
|
|
22
|
+
@range.format(on_sheet: on_sheet),
|
|
23
|
+
]
|
|
24
|
+
args << @match_type.serialize if @match_type.present?
|
|
25
|
+
"MATCH(#{args.join(',')})"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::Max < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig { params(values: T::Array[T.any(Zaxcel::Cell::ValueType, Zaxcel::References::Range)]).void }
|
|
8
|
+
def initialize(values)
|
|
9
|
+
@values = values
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
13
|
+
def format(on_sheet:)
|
|
14
|
+
formatted_values = @values.map { |value| Zaxcel::Cell.format(value, on_sheet: on_sheet) }.compact
|
|
15
|
+
"MAX(#{formatted_values.join(',')})"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::Min < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig { params(values: T::Array[T.any(Zaxcel::Cell::ValueType, Zaxcel::References::Range)]).void }
|
|
8
|
+
def initialize(values)
|
|
9
|
+
@values = values
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
13
|
+
def format(on_sheet:)
|
|
14
|
+
formatted_values = @values.map { |value| Zaxcel::Cell.format(value, on_sheet: on_sheet) }.compact
|
|
15
|
+
"MIN(#{formatted_values.join(',')})"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
# This isn't really a function, it's a unary operator, so this is in the wrong spot. But I'm not sure where the best
|
|
5
|
+
# place for it is yet, so just leave it here for now.
|
|
6
|
+
class Zaxcel::Functions::Negate < Zaxcel::CellFormula
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
sig { params(value: Zaxcel::Cell::ValueType).void }
|
|
10
|
+
def initialize(value)
|
|
11
|
+
@value = value
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
15
|
+
def format(on_sheet:)
|
|
16
|
+
formatted_value = Zaxcel::Cell.format(@value, on_sheet: on_sheet)
|
|
17
|
+
return '0' if formatted_value.nil? && @value.is_a?(Zaxcel::References::Cell)
|
|
18
|
+
|
|
19
|
+
formatted_value = "(#{formatted_value})" if @value.is_a?(Zaxcel::BinaryExpression) && [
|
|
20
|
+
'-',
|
|
21
|
+
'+',
|
|
22
|
+
].include?(@value.operator)
|
|
23
|
+
|
|
24
|
+
"-#{formatted_value}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::Or < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
# todo - likely and can accept more than one arg, but not sure. check into this.
|
|
8
|
+
# also, it may accept only specific types of formulas (returns true or false or something), but my suspicion is that
|
|
9
|
+
# is not true. confirm this also, since right now it's typed to a strange type.
|
|
10
|
+
sig { params(left_hand_value: Zaxcel::Cell::ValueType, right_hand_value: Zaxcel::Cell::ValueType).void }
|
|
11
|
+
def initialize(left_hand_value, right_hand_value)
|
|
12
|
+
@left_hand_value = left_hand_value
|
|
13
|
+
@right_hand_value = right_hand_value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
17
|
+
def format(on_sheet:)
|
|
18
|
+
left_hand_formatted_value = Zaxcel::Cell.format(@left_hand_value, on_sheet: on_sheet)
|
|
19
|
+
right_hand_formatted_value = Zaxcel::Cell.format(@right_hand_value, on_sheet: on_sheet)
|
|
20
|
+
|
|
21
|
+
"OR(#{left_hand_formatted_value},#{right_hand_formatted_value})"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::Round < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig { params(value: Zaxcel::Cell::ValueType, precision: Integer).void }
|
|
8
|
+
def initialize(value, precision: 0)
|
|
9
|
+
@value = value
|
|
10
|
+
@precision = precision
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
14
|
+
def format(on_sheet:)
|
|
15
|
+
formatted_value = Zaxcel::Cell.format(@value, on_sheet: on_sheet)
|
|
16
|
+
return '0' if formatted_value.nil? && @value.is_a?(Zaxcel::References::Cell)
|
|
17
|
+
|
|
18
|
+
"ROUND(#{formatted_value},#{@precision})"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::Sum < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig { params(values: T::Array[T.any(Zaxcel::Cell::ValueType, Zaxcel::References::Range)]).void }
|
|
8
|
+
def initialize(values)
|
|
9
|
+
@values = values
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
13
|
+
def format(on_sheet:)
|
|
14
|
+
return '0' if @values.blank?
|
|
15
|
+
|
|
16
|
+
"SUM(#{@values.map { |value| Zaxcel::Cell.format(value, on_sheet: on_sheet) }.join(',')})"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::SumIf < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig { params(range_to_check: Zaxcel::References::Range, value_to_check: Zaxcel::Cell::ValueType, range_to_sum: Zaxcel::References::Range).void }
|
|
8
|
+
def initialize(range_to_check:, value_to_check:, range_to_sum:)
|
|
9
|
+
raise 'Column to check and column to sum must be on the same sheet!' if range_to_check.sheet_name != range_to_sum.sheet_name
|
|
10
|
+
|
|
11
|
+
@range_to_check = range_to_check
|
|
12
|
+
@value_to_check = value_to_check
|
|
13
|
+
@range_to_sum = range_to_sum
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
17
|
+
def format(on_sheet:)
|
|
18
|
+
"SUMIF(#{@range_to_check.format(on_sheet: on_sheet)},#{Zaxcel::Cell.format(@value_to_check, on_sheet: on_sheet)},#{@range_to_sum.format(on_sheet: on_sheet)})"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::SumIfs < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig do
|
|
8
|
+
params(
|
|
9
|
+
ranges_to_check: T::Array[Zaxcel::References::Range],
|
|
10
|
+
values_to_check: T::Array[Zaxcel::Cell::ValueType],
|
|
11
|
+
range_to_sum: Zaxcel::References::Range,
|
|
12
|
+
).void
|
|
13
|
+
end
|
|
14
|
+
def initialize(ranges_to_check:, values_to_check:, range_to_sum:)
|
|
15
|
+
raise 'Columns to check and values to check must be the same length' if ranges_to_check.count != values_to_check.count
|
|
16
|
+
|
|
17
|
+
@ranges_to_check = ranges_to_check
|
|
18
|
+
@values_to_check = values_to_check
|
|
19
|
+
@range_to_sum = range_to_sum
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
23
|
+
def format(on_sheet:)
|
|
24
|
+
criteria = @ranges_to_check.flat_map.with_index do |check_range, index|
|
|
25
|
+
check_value = @values_to_check[index]
|
|
26
|
+
[
|
|
27
|
+
check_range.format(on_sheet: on_sheet),
|
|
28
|
+
Zaxcel::Cell.format(check_value, on_sheet: on_sheet, quote_strings: false),
|
|
29
|
+
]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
"SUMIFS(#{@range_to_sum.format(on_sheet: on_sheet)},#{criteria.join(',')})"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::SumProduct < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig { params(values: T::Array[T.any(Zaxcel::Cell::ValueType, Zaxcel::References::Range)]).void }
|
|
8
|
+
def initialize(values)
|
|
9
|
+
@values = values
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
13
|
+
def format(on_sheet:)
|
|
14
|
+
return '0' if @values.blank?
|
|
15
|
+
|
|
16
|
+
"SUMPRODUCT(#{@values.map { |value| Zaxcel::Cell.format(value, on_sheet: on_sheet) }.join(',')})"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::Text < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig { params(value: Zaxcel::Cell::ValueType, format_string: String).void }
|
|
8
|
+
def initialize(value, format_string:)
|
|
9
|
+
@value = value
|
|
10
|
+
@format_string = format_string
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
14
|
+
def format(on_sheet:)
|
|
15
|
+
"TEXT(#{Zaxcel::Cell.format(@value, on_sheet: on_sheet)},#{Zaxcel::Cell.format(@format_string, on_sheet: on_sheet)})"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::Unique < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig { params(range: Zaxcel::References::Range, array_formula: T::Boolean).void }
|
|
8
|
+
def initialize(range, array_formula: false)
|
|
9
|
+
@range = range
|
|
10
|
+
@array_formula = array_formula
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
14
|
+
def format(on_sheet:)
|
|
15
|
+
"UNIQUE(#{@range.format(on_sheet: on_sheet)})"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# We'll probably want to abstract this into an interface or common class or something, but for now just define here
|
|
19
|
+
sig { returns(T::Boolean) }
|
|
20
|
+
def array_formula?
|
|
21
|
+
@array_formula
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Functions::XLookup < Zaxcel::Function
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig do
|
|
8
|
+
params(
|
|
9
|
+
condition: Zaxcel::Cell::ValueType,
|
|
10
|
+
idx_range: Zaxcel::References::Range,
|
|
11
|
+
value_range: Zaxcel::References::Range,
|
|
12
|
+
).void
|
|
13
|
+
end
|
|
14
|
+
def initialize(condition, idx_range:, value_range:)
|
|
15
|
+
@condition = condition
|
|
16
|
+
@idx_range = idx_range
|
|
17
|
+
@value_range = value_range
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig { override.params(on_sheet: String).returns(String) }
|
|
21
|
+
def format(on_sheet:)
|
|
22
|
+
formatted_condition = Zaxcel::Cell.format(@condition, on_sheet: on_sheet)
|
|
23
|
+
formatted_idx_range = @idx_range.format(on_sheet: on_sheet)
|
|
24
|
+
formatted_value_range = @value_range.format(on_sheet: on_sheet)
|
|
25
|
+
|
|
26
|
+
"XLOOKUP(#{formatted_condition},#{formatted_idx_range},#{formatted_value_range})"
|
|
27
|
+
end
|
|
28
|
+
end
|