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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +4 -0
  3. data/.rubocop.yml +9 -0
  4. data/CHANGELOG.md +29 -0
  5. data/CONTRIBUTING.md +110 -0
  6. data/LICENSE +22 -0
  7. data/QUICK_START.md +187 -0
  8. data/README.md +372 -0
  9. data/Rakefile +18 -0
  10. data/SETUP.md +178 -0
  11. data/lib/enumerable.rb +47 -0
  12. data/lib/zaxcel/README.md +37 -0
  13. data/lib/zaxcel/arithmetic.rb +88 -0
  14. data/lib/zaxcel/binary_expression.rb +74 -0
  15. data/lib/zaxcel/binary_expressions/addition.rb +36 -0
  16. data/lib/zaxcel/binary_expressions/division.rb +24 -0
  17. data/lib/zaxcel/binary_expressions/multiplication.rb +24 -0
  18. data/lib/zaxcel/binary_expressions/subtraction.rb +41 -0
  19. data/lib/zaxcel/binary_expressions.rb +38 -0
  20. data/lib/zaxcel/cell.rb +141 -0
  21. data/lib/zaxcel/cell_formula.rb +16 -0
  22. data/lib/zaxcel/column.rb +142 -0
  23. data/lib/zaxcel/document.rb +136 -0
  24. data/lib/zaxcel/function.rb +6 -0
  25. data/lib/zaxcel/functions/abs.rb +18 -0
  26. data/lib/zaxcel/functions/and.rb +23 -0
  27. data/lib/zaxcel/functions/average.rb +17 -0
  28. data/lib/zaxcel/functions/choose.rb +20 -0
  29. data/lib/zaxcel/functions/concatenate.rb +20 -0
  30. data/lib/zaxcel/functions/if.rb +38 -0
  31. data/lib/zaxcel/functions/if_error.rb +25 -0
  32. data/lib/zaxcel/functions/index.rb +20 -0
  33. data/lib/zaxcel/functions/len.rb +16 -0
  34. data/lib/zaxcel/functions/match/match_type.rb +13 -0
  35. data/lib/zaxcel/functions/match.rb +27 -0
  36. data/lib/zaxcel/functions/max.rb +17 -0
  37. data/lib/zaxcel/functions/min.rb +17 -0
  38. data/lib/zaxcel/functions/negate.rb +26 -0
  39. data/lib/zaxcel/functions/or.rb +23 -0
  40. data/lib/zaxcel/functions/round.rb +20 -0
  41. data/lib/zaxcel/functions/sum.rb +18 -0
  42. data/lib/zaxcel/functions/sum_if.rb +20 -0
  43. data/lib/zaxcel/functions/sum_ifs.rb +34 -0
  44. data/lib/zaxcel/functions/sum_product.rb +18 -0
  45. data/lib/zaxcel/functions/text.rb +17 -0
  46. data/lib/zaxcel/functions/unique.rb +23 -0
  47. data/lib/zaxcel/functions/x_lookup.rb +28 -0
  48. data/lib/zaxcel/functions/xirr.rb +27 -0
  49. data/lib/zaxcel/functions.rb +169 -0
  50. data/lib/zaxcel/if_builder.rb +22 -0
  51. data/lib/zaxcel/lang.rb +23 -0
  52. data/lib/zaxcel/reference.rb +28 -0
  53. data/lib/zaxcel/references/cell.rb +42 -0
  54. data/lib/zaxcel/references/column.rb +49 -0
  55. data/lib/zaxcel/references/range.rb +35 -0
  56. data/lib/zaxcel/references/row.rb +34 -0
  57. data/lib/zaxcel/references.rb +5 -0
  58. data/lib/zaxcel/roundable.rb +14 -0
  59. data/lib/zaxcel/row.rb +93 -0
  60. data/lib/zaxcel/sheet.rb +425 -0
  61. data/lib/zaxcel/sorbet/enumerizable_enum.rb +50 -0
  62. data/lib/zaxcel/version.rb +6 -0
  63. data/lib/zaxcel.rb +73 -0
  64. data/zaxcel.gemspec +73 -0
  65. 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