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,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module Zaxcel::Arithmetic
|
|
5
|
+
include Kernel
|
|
6
|
+
include ActiveSupport::Tryable
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
class CoercedValue < T::Struct
|
|
10
|
+
extend T::Sig
|
|
11
|
+
include Zaxcel::Arithmetic
|
|
12
|
+
|
|
13
|
+
const(:value, T.any(Numeric, Money))
|
|
14
|
+
|
|
15
|
+
sig { params(on_sheet: String).returns(T.any(Numeric, Money)) }
|
|
16
|
+
def format(on_sheet:)
|
|
17
|
+
value
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig { returns(T::Boolean) }
|
|
21
|
+
def additive_identity?
|
|
22
|
+
object_id == Zero.object_id
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Zero = CoercedValue.new(value: 0)
|
|
27
|
+
private_constant :Zero
|
|
28
|
+
|
|
29
|
+
#
|
|
30
|
+
# https://docs.ruby-lang.org/en/master/Numeric.html
|
|
31
|
+
#
|
|
32
|
+
# > Classes which inherit from Numeric must implement coerce, which returns a two-member Array containing an object
|
|
33
|
+
# > that has been coerced into an instance of the new class and self (see coerce).
|
|
34
|
+
# >
|
|
35
|
+
# > Inheriting classes should also implement arithmetic operator methods (+, -, * and /) and the <=> operator
|
|
36
|
+
# > (see Comparable). These methods may rely on coerce to ensure interoperability with instances of other numeric
|
|
37
|
+
# > classes.
|
|
38
|
+
#
|
|
39
|
+
# Inheriting from Numeric allows us to add Zaxcel objects like cell references and formulas to Ruby numerics.
|
|
40
|
+
# For example:
|
|
41
|
+
# `1 + cell_ref` will call coerce on `1` and return a `Zaxcel::CellFormula` object representing the sum.
|
|
42
|
+
|
|
43
|
+
sig { params(other: T.any(Numeric, Money)).returns([CoercedValue, Zaxcel::Arithmetic]) }
|
|
44
|
+
def coerce(other)
|
|
45
|
+
[CoercedValue.new(value: other), self]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# These need to be implemented as part of the Numeric interface.
|
|
49
|
+
|
|
50
|
+
sig { returns(Zaxcel::CellFormula) }
|
|
51
|
+
def -@
|
|
52
|
+
Zaxcel::Functions::Negate.new(self)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { params(other: T.any(Numeric, Money, Zaxcel::Arithmetic)).returns(Zaxcel::Arithmetic) }
|
|
56
|
+
def +(other)
|
|
57
|
+
Zaxcel::BinaryExpressions::Addition.new(self, other)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig { params(other: T.any(Numeric, Money, Zaxcel::Arithmetic)).returns(Zaxcel::CellFormula) }
|
|
61
|
+
def -(other)
|
|
62
|
+
Zaxcel::BinaryExpressions::Subtraction.new(self, other)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
sig { params(other: T.any(Numeric, Money, Zaxcel::Arithmetic)).returns(Zaxcel::CellFormula) }
|
|
66
|
+
def *(other)
|
|
67
|
+
Zaxcel::BinaryExpressions::Multiplication.new(self, other)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sig { params(other: T.any(Numeric, Money, Zaxcel::Arithmetic)).returns(Zaxcel::CellFormula) }
|
|
71
|
+
def /(other)
|
|
72
|
+
Zaxcel::BinaryExpressions::Division.new(self, other)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
sig { returns(TrueClass) }
|
|
76
|
+
def present?
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class << self
|
|
81
|
+
extend T::Sig
|
|
82
|
+
|
|
83
|
+
sig { returns(Zaxcel::Arithmetic) }
|
|
84
|
+
def zero
|
|
85
|
+
Zero
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::BinaryExpression < Zaxcel::CellFormula
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig { returns(String) }
|
|
8
|
+
attr_reader :operator
|
|
9
|
+
|
|
10
|
+
sig do
|
|
11
|
+
params(
|
|
12
|
+
operator: String,
|
|
13
|
+
lh_value: Zaxcel::Cell::ValueType,
|
|
14
|
+
rh_value: Zaxcel::Cell::ValueType,
|
|
15
|
+
).void
|
|
16
|
+
end
|
|
17
|
+
def initialize(operator, lh_value, rh_value)
|
|
18
|
+
@operator = operator
|
|
19
|
+
@lh_value = lh_value
|
|
20
|
+
@rh_value = rh_value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
sig { override.params(on_sheet: String).returns(T.nilable(T.any(Numeric, Money, String))) }
|
|
24
|
+
def format(on_sheet:)
|
|
25
|
+
formatted_lh_value = format_value(@lh_value, on_sheet: on_sheet)
|
|
26
|
+
formatted_rh_value = format_value(@rh_value, on_sheet: on_sheet)
|
|
27
|
+
|
|
28
|
+
"#{formatted_lh_value}#{@operator}#{formatted_rh_value}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
sig do
|
|
34
|
+
params(
|
|
35
|
+
value: Zaxcel::Cell::ValueType,
|
|
36
|
+
on_sheet: String,
|
|
37
|
+
).returns(T.any(Numeric, Money, String))
|
|
38
|
+
end
|
|
39
|
+
def format_value(value, on_sheet:)
|
|
40
|
+
base_format = Zaxcel::Cell.format(value, on_sheet: on_sheet)
|
|
41
|
+
# If a cell reference doesn't resolve, we want to handle it gracefully and not produce bad formulas. If we just
|
|
42
|
+
# print an empty string, we will get a bad formula, so instead swap for zero, since it is reasonable to assume that
|
|
43
|
+
# if a cell does not exist, its value is zero.
|
|
44
|
+
# In the future, we might consider adding a default behavior of erroring on missing cell references, with the
|
|
45
|
+
# ability to add an if_error formula which replaces a nil cell reference with a default value.
|
|
46
|
+
return '0' if base_format.nil?
|
|
47
|
+
|
|
48
|
+
base_format = "(#{base_format})" if wrap_value?(value)
|
|
49
|
+
base_format
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Operators don't always associate with each other nicely with parentheses. For instance,
|
|
53
|
+
# 3 * (1 + 2) != 3 * 1 + 2,
|
|
54
|
+
# so we need some rules around when to wrap in parens when the left or right hand value have operators.
|
|
55
|
+
# The general rules are:
|
|
56
|
+
# 1. When it's a basic value or a reference, such as string or number, don't wrap, e.g. "hello", 1, or A1:B1.
|
|
57
|
+
# 2. When it's a function, don't wrap since the function is a single token, e.g. SUM(...), MIN(...), or ROUND(...).
|
|
58
|
+
# 3. When it's a binary expression, wrap if the operator distributes over the inner operator.
|
|
59
|
+
# https://en.wikipedia.org/wiki/Distributive_property
|
|
60
|
+
sig { params(value: Zaxcel::Cell::ValueType).returns(T::Boolean) }
|
|
61
|
+
def wrap_value?(value)
|
|
62
|
+
return false if !value.is_a?(Zaxcel::CellFormula)
|
|
63
|
+
return true if value.is_a?(Zaxcel::Functions::Negate)
|
|
64
|
+
return false if value.is_a?(Zaxcel::Function)
|
|
65
|
+
return distributive?(value.operator) if value.is_a?(Zaxcel::BinaryExpression)
|
|
66
|
+
|
|
67
|
+
true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sig { overridable.params(inner_operator: String).returns(T::Boolean) }
|
|
71
|
+
def distributive?(inner_operator)
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::BinaryExpressions::Addition < Zaxcel::BinaryExpression
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig do
|
|
8
|
+
params(
|
|
9
|
+
lh_value: Zaxcel::Cell::ValueType,
|
|
10
|
+
rh_value: Zaxcel::Cell::ValueType,
|
|
11
|
+
).void
|
|
12
|
+
end
|
|
13
|
+
def initialize(lh_value, rh_value)
|
|
14
|
+
super('+', lh_value, rh_value)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { override.params(on_sheet: String).returns(T.nilable(T.any(Numeric, Money, String))) }
|
|
18
|
+
def format(on_sheet:)
|
|
19
|
+
# Don't call `format_value` here to avoid wrapping in parens, which is unnecessary.
|
|
20
|
+
if @lh_value.is_a?(Zaxcel::Arithmetic::CoercedValue) && @lh_value.additive_identity?
|
|
21
|
+
return Zaxcel::Cell.format(@rh_value, on_sheet: on_sheet)
|
|
22
|
+
elsif @rh_value.is_a?(Zaxcel::Arithmetic::CoercedValue) && @rh_value.additive_identity?
|
|
23
|
+
return Zaxcel::Cell.format(@lh_value, on_sheet: on_sheet)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# sum never distributes over anything (other things distribute over it)
|
|
32
|
+
sig { override.params(inner_operator: String).returns(T::Boolean) }
|
|
33
|
+
def distributive?(inner_operator)
|
|
34
|
+
false
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::BinaryExpressions::Division < Zaxcel::BinaryExpression
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig do
|
|
8
|
+
params(
|
|
9
|
+
lh_value: Zaxcel::Cell::ValueType,
|
|
10
|
+
rh_value: Zaxcel::Cell::ValueType,
|
|
11
|
+
).void
|
|
12
|
+
end
|
|
13
|
+
def initialize(lh_value, rh_value)
|
|
14
|
+
super('/', lh_value, rh_value)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# division distributes over addition / subtraction
|
|
20
|
+
sig { override.params(inner_operator: String).returns(T::Boolean) }
|
|
21
|
+
def distributive?(inner_operator)
|
|
22
|
+
['+', '-'].include?(inner_operator)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::BinaryExpressions::Multiplication < Zaxcel::BinaryExpression
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig do
|
|
8
|
+
params(
|
|
9
|
+
lh_value: Zaxcel::Cell::ValueType,
|
|
10
|
+
rh_value: Zaxcel::Cell::ValueType,
|
|
11
|
+
).void
|
|
12
|
+
end
|
|
13
|
+
def initialize(lh_value, rh_value)
|
|
14
|
+
super('*', lh_value, rh_value)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# multiplication distributes over addition / subtraction
|
|
20
|
+
sig { override.params(inner_operator: String).returns(T::Boolean) }
|
|
21
|
+
def distributive?(inner_operator)
|
|
22
|
+
['+', '-'].include?(inner_operator)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::BinaryExpressions::Subtraction < Zaxcel::BinaryExpression
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig do
|
|
8
|
+
params(
|
|
9
|
+
lh_value: Zaxcel::Cell::ValueType,
|
|
10
|
+
rh_value: Zaxcel::Cell::ValueType,
|
|
11
|
+
).void
|
|
12
|
+
end
|
|
13
|
+
def initialize(lh_value, rh_value)
|
|
14
|
+
super('-', lh_value, rh_value)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { override.params(on_sheet: String).returns(T.nilable(T.any(Numeric, Money, String))) }
|
|
18
|
+
def format(on_sheet:)
|
|
19
|
+
# Don't call `format_value` here to avoid wrapping in parens, which is unnecessary.
|
|
20
|
+
if @lh_value.is_a?(Zaxcel::Arithmetic::CoercedValue) && @lh_value.additive_identity?
|
|
21
|
+
# stupid, but ValueType technically can be true, false, date, time, etc.
|
|
22
|
+
if @rh_value.is_a?(Numeric) || @rh_value.is_a?(Money) || @rh_value.is_a?(Zaxcel::Arithmetic)
|
|
23
|
+
@rh_value = -@rh_value
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
return Zaxcel::Cell.format(@rh_value, on_sheet: on_sheet)
|
|
27
|
+
elsif @rh_value.is_a?(Zaxcel::Arithmetic::CoercedValue) && @rh_value.additive_identity?
|
|
28
|
+
return Zaxcel::Cell.format(@lh_value, on_sheet: on_sheet)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# subtraction distributes over addition / subtraction (technically it's -1 * (...))
|
|
37
|
+
sig { override.params(inner_operator: String).returns(T::Boolean) }
|
|
38
|
+
def distributive?(inner_operator)
|
|
39
|
+
['+', '-'].include?(inner_operator)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module Zaxcel::BinaryExpressions
|
|
5
|
+
class << self
|
|
6
|
+
extend T::Sig
|
|
7
|
+
|
|
8
|
+
sig { params(lh_value: Zaxcel::Cell::ValueType, rh_value: Zaxcel::Cell::ValueType).returns(Zaxcel::BinaryExpression) }
|
|
9
|
+
def less_than(lh_value, rh_value)
|
|
10
|
+
Zaxcel::BinaryExpression.new('<', lh_value, rh_value)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
sig { params(lh_value: Zaxcel::Cell::ValueType, rh_value: Zaxcel::Cell::ValueType).returns(Zaxcel::BinaryExpression) }
|
|
14
|
+
def less_than_equal(lh_value, rh_value)
|
|
15
|
+
Zaxcel::BinaryExpression.new('<=', lh_value, rh_value)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { params(lh_value: Zaxcel::Cell::ValueType, rh_value: Zaxcel::Cell::ValueType).returns(Zaxcel::BinaryExpression) }
|
|
19
|
+
def greater_than(lh_value, rh_value)
|
|
20
|
+
Zaxcel::BinaryExpression.new('>', lh_value, rh_value)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
sig { params(lh_value: Zaxcel::Cell::ValueType, rh_value: Zaxcel::Cell::ValueType).returns(Zaxcel::BinaryExpression) }
|
|
24
|
+
def greater_than_equal(lh_value, rh_value)
|
|
25
|
+
Zaxcel::BinaryExpression.new('>=', lh_value, rh_value)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { params(lh_value: Zaxcel::Cell::ValueType, rh_value: Zaxcel::Cell::ValueType).returns(Zaxcel::BinaryExpression) }
|
|
29
|
+
def equal(lh_value, rh_value)
|
|
30
|
+
Zaxcel::BinaryExpression.new('=', lh_value, rh_value)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sig { params(lh_value: Zaxcel::Cell::ValueType, rh_value: Zaxcel::Cell::ValueType).returns(Zaxcel::BinaryExpression) }
|
|
34
|
+
def not_equal(lh_value, rh_value)
|
|
35
|
+
Zaxcel::BinaryExpression.new('<>', lh_value, rh_value)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/zaxcel/cell.rb
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Cell
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
ConstantValueType = T.type_alias { T.any(NilClass, Numeric, Money, String, Date, Time, TrueClass, FalseClass) }
|
|
8
|
+
ValueType = T.type_alias { T.any(ConstantValueType, Zaxcel::Arithmetic) }
|
|
9
|
+
CellExtractionKey = T.type_alias { [String, Symbol, Symbol] }
|
|
10
|
+
|
|
11
|
+
EMPTY_VALUE = ''
|
|
12
|
+
|
|
13
|
+
sig { returns(Symbol) }
|
|
14
|
+
attr_reader :style
|
|
15
|
+
|
|
16
|
+
sig { returns(T.nilable(ValueType)) }
|
|
17
|
+
attr_reader :value
|
|
18
|
+
|
|
19
|
+
sig { returns(Zaxcel::Column) }
|
|
20
|
+
attr_reader :column
|
|
21
|
+
|
|
22
|
+
sig { returns(Zaxcel::Row) }
|
|
23
|
+
attr_reader :row
|
|
24
|
+
|
|
25
|
+
sig { returns(T::Boolean) }
|
|
26
|
+
attr_reader :to_extract
|
|
27
|
+
|
|
28
|
+
sig { params(column: Zaxcel::Column, row: Zaxcel::Row, style: T.nilable(Symbol), value: ValueType, to_extract: T::Boolean).void }
|
|
29
|
+
def initialize(column:, row:, style:, value:, to_extract: false)
|
|
30
|
+
@column = column
|
|
31
|
+
@row = row
|
|
32
|
+
@value = value
|
|
33
|
+
@to_extract = to_extract
|
|
34
|
+
# style can be nil, the name of a style, or the name of a style group
|
|
35
|
+
# if it's nil, fall back to the style_group on the row
|
|
36
|
+
style ||= row.style_group
|
|
37
|
+
# first check if it's a style group, if not, then it's a style
|
|
38
|
+
style = column.try(style) || style
|
|
39
|
+
|
|
40
|
+
@style = T.let(style, Symbol)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sig { returns(String) }
|
|
44
|
+
def to_excel
|
|
45
|
+
if col_position.nil? && row_position.nil?
|
|
46
|
+
return '0' if @value.nil? || @value.try(:zero?)
|
|
47
|
+
|
|
48
|
+
raise 'Cannot call to_excel until col and row indices are set. Call position! first'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
"#{@column.to_excel}#{@row.to_excel}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
sig { returns(T.nilable(Integer)) }
|
|
55
|
+
def row_position
|
|
56
|
+
@row.position
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sig { returns(T.nilable(Integer)) }
|
|
60
|
+
def col_position
|
|
61
|
+
@column.position
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
sig { returns(T.nilable(Integer)) }
|
|
65
|
+
def estimated_formatted_character_length
|
|
66
|
+
self.class.estimated_character_length_from_cell_value(@value)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sig { returns(T::Boolean) }
|
|
70
|
+
def to_extract?
|
|
71
|
+
@to_extract
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sig { returns(CellExtractionKey) }
|
|
75
|
+
def extraction_key
|
|
76
|
+
[column.sheet.name, row.name, column.name]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class << self
|
|
80
|
+
extend T::Sig
|
|
81
|
+
|
|
82
|
+
sig { params(value: T.nilable(ValueType)).returns(T.nilable(Integer)) }
|
|
83
|
+
def estimated_character_length_from_cell_value(value)
|
|
84
|
+
case value
|
|
85
|
+
when String
|
|
86
|
+
value.length
|
|
87
|
+
when Integer, Float, Money, BigDecimal
|
|
88
|
+
# Add 2 for potential negative sign and decimal point
|
|
89
|
+
# Add 1 character per 3 digits for commas
|
|
90
|
+
num_str = value.to_s.gsub(/[.-]/, '')
|
|
91
|
+
num_str.length + 2 + (num_str.length / 3.0).ceil
|
|
92
|
+
when Time
|
|
93
|
+
value.strftime('%m/%d/%Y %H:%M:%S').length
|
|
94
|
+
when Date
|
|
95
|
+
format_date(value).length
|
|
96
|
+
when TrueClass, FalseClass
|
|
97
|
+
value.to_s.length
|
|
98
|
+
when Zaxcel::CellFormula, Zaxcel::References::Cell
|
|
99
|
+
# For formulas and references, we don't know the length until we evaluate them
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
sig do
|
|
105
|
+
params(
|
|
106
|
+
value: T.nilable(T.any(ValueType, Zaxcel::References::Range)),
|
|
107
|
+
on_sheet: String,
|
|
108
|
+
quote_strings: T::Boolean,
|
|
109
|
+
).returns(T.nilable(T.any(Numeric, Money, String)))
|
|
110
|
+
end
|
|
111
|
+
def format(value, on_sheet:, quote_strings: true)
|
|
112
|
+
case value
|
|
113
|
+
when String
|
|
114
|
+
# don't set empty cells to empty string - this allows other cells to overflow into them
|
|
115
|
+
return if value.empty?
|
|
116
|
+
|
|
117
|
+
# Strings inside of formulas need to be escaped with quotes, otherwise they cause errors. Strings printed
|
|
118
|
+
# directly into the document (not as part of a formula) should not be escaped.
|
|
119
|
+
quote_strings ? "\"#{value}\"" : value
|
|
120
|
+
when Numeric, Money
|
|
121
|
+
value
|
|
122
|
+
# this must come before date because Time < Date
|
|
123
|
+
when Time
|
|
124
|
+
"DATE(#{value.year}, #{value.month}, #{value.day})+TIME(#{value.hour}, #{value.min}, #{value.sec})"
|
|
125
|
+
when Date
|
|
126
|
+
format_date(value)
|
|
127
|
+
when TrueClass
|
|
128
|
+
'TRUE'
|
|
129
|
+
when FalseClass
|
|
130
|
+
'FALSE'
|
|
131
|
+
when Zaxcel::CellFormula, Zaxcel::References::Cell, Zaxcel::References::Range, Zaxcel::Arithmetic::CoercedValue
|
|
132
|
+
value.format(on_sheet: on_sheet)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
sig { params(value: Date).returns(String) }
|
|
137
|
+
def format_date(value)
|
|
138
|
+
value.strftime('%m/%d/%Y')
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::CellFormula
|
|
5
|
+
extend T::Sig
|
|
6
|
+
extend T::Helpers
|
|
7
|
+
include Zaxcel::Arithmetic
|
|
8
|
+
include Zaxcel::Roundable
|
|
9
|
+
|
|
10
|
+
abstract!
|
|
11
|
+
|
|
12
|
+
EXCEL_EPOCH = Date.new(1900, 1, 1)
|
|
13
|
+
|
|
14
|
+
sig { abstract.params(on_sheet: String).returns(T.nilable(T.any(Numeric, Money, String))) }
|
|
15
|
+
def format(on_sheet:); end
|
|
16
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Column
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
DEFAULT_COLUMN_WIDTH = 20
|
|
8
|
+
class ComputedColumnWidth < T::Enum
|
|
9
|
+
enums do
|
|
10
|
+
MaxContent = new
|
|
11
|
+
Header = new
|
|
12
|
+
HeaderTwoLines = new
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
sig { returns(Symbol) }
|
|
17
|
+
attr_reader :name
|
|
18
|
+
|
|
19
|
+
sig { returns(String) }
|
|
20
|
+
attr_reader :header
|
|
21
|
+
|
|
22
|
+
sig { returns(Symbol) }
|
|
23
|
+
attr_reader :header_style
|
|
24
|
+
|
|
25
|
+
sig { returns(Symbol) }
|
|
26
|
+
attr_reader :row_style
|
|
27
|
+
|
|
28
|
+
sig { returns(Symbol) }
|
|
29
|
+
attr_reader :alt_row_style
|
|
30
|
+
|
|
31
|
+
sig { returns(Symbol) }
|
|
32
|
+
attr_reader :first_row_style
|
|
33
|
+
|
|
34
|
+
sig { returns(T.nilable(Symbol)) }
|
|
35
|
+
attr_reader :total_style
|
|
36
|
+
|
|
37
|
+
# You can specify `nil` and CAXLSX will auto-calculate an appropriate width based on the column's contents.
|
|
38
|
+
sig { returns(T.nilable(T.any(Integer, Float, ComputedColumnWidth))) }
|
|
39
|
+
attr_reader :width
|
|
40
|
+
|
|
41
|
+
sig { returns(T.nilable(T.any(Integer, Float))) }
|
|
42
|
+
attr_reader :min_width
|
|
43
|
+
|
|
44
|
+
sig { returns(Zaxcel::Sheet) }
|
|
45
|
+
attr_reader :sheet
|
|
46
|
+
|
|
47
|
+
sig { params(sheet: Zaxcel::Sheet, name: T.any(Symbol, String), header: String, header_style: Symbol, row_style: Symbol, first_row_style: Symbol, total_style: T.nilable(Symbol), alt_row_style: T.nilable(Symbol), width: T.nilable(T.any(Integer, Float, ComputedColumnWidth)), min_width: T.nilable(T.any(Integer, Float))).void }
|
|
48
|
+
def initialize(sheet:, name:, header:, header_style:, row_style:, first_row_style:, total_style:, alt_row_style: nil, width: DEFAULT_COLUMN_WIDTH, min_width: 0)
|
|
49
|
+
@sheet = sheet
|
|
50
|
+
@name = T.let(name.to_sym, Symbol)
|
|
51
|
+
@header = header
|
|
52
|
+
@header_style = header_style
|
|
53
|
+
@row_style = row_style
|
|
54
|
+
@alt_row_style = T.let(alt_row_style || row_style, Symbol)
|
|
55
|
+
@first_row_style = first_row_style
|
|
56
|
+
@total_style = total_style
|
|
57
|
+
@width = width
|
|
58
|
+
@min_width = min_width
|
|
59
|
+
@print_boundary = T.let(false, T::Boolean)
|
|
60
|
+
@hidden = T.let(false, T::Boolean)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { params(row_name: T.any(String, Symbol), sheet_name: T.nilable(String)).returns(Zaxcel::References::Cell) }
|
|
64
|
+
def ref(row_name, sheet_name: nil)
|
|
65
|
+
Zaxcel::References::Cell.new(document: @sheet.document, sheet_name: sheet_name || @sheet.name, col_name: @name, row_name: row_name.to_sym)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
sig { params(row: Zaxcel::Row, value: T.nilable(Zaxcel::Cell::ValueType), style: T.nilable(Symbol), to_extract: T::Boolean).returns(Zaxcel::Cell) }
|
|
69
|
+
def add_cell!(row:, value: nil, style: nil, to_extract: false)
|
|
70
|
+
cell_by_row_name[row.name] = Zaxcel::Cell.new(
|
|
71
|
+
column: self,
|
|
72
|
+
row: row,
|
|
73
|
+
style: style,
|
|
74
|
+
value: value,
|
|
75
|
+
to_extract: to_extract,
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
sig { params(row_name: T.any(Symbol, String)).returns(T.nilable(Zaxcel::Cell)) }
|
|
80
|
+
def cell(row_name)
|
|
81
|
+
cell_by_row_name[row_name.to_sym]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
sig { params(column_position: Integer).void }
|
|
85
|
+
def position!(column_position)
|
|
86
|
+
@position = T.let(column_position, T.nilable(Integer))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
sig { returns(T.nilable(Integer)) }
|
|
90
|
+
def position
|
|
91
|
+
@position
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Excel uses letters for column names. When it reaches 'Z', it starts over with 'AA', 'AB', etc.
|
|
95
|
+
# until it hits 'AZ' and then moves on to 'BZ'. Eventually, it will reach 'ZZ' and start 'AAA'.
|
|
96
|
+
# This method converts the column position to the appropriate sequence of letters.
|
|
97
|
+
sig { returns(String) }
|
|
98
|
+
def to_excel
|
|
99
|
+
raise 'Must position cells before calling to_excel' if @position.nil?
|
|
100
|
+
|
|
101
|
+
self.class.base_26_string(@position)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
sig { returns(T::Hash[Symbol, Zaxcel::Cell]) }
|
|
105
|
+
def cell_by_row_name
|
|
106
|
+
@cell_by_row_name ||= T.let({}, T.nilable(T::Hash[Symbol, Zaxcel::Cell]))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
sig { void }
|
|
110
|
+
def hide!
|
|
111
|
+
@hidden = true
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
sig { returns(T::Boolean) }
|
|
115
|
+
def hidden?
|
|
116
|
+
@hidden
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
sig { void }
|
|
120
|
+
def set_print_boundary!
|
|
121
|
+
raise 'Print boundary column already exists' if @sheet.print_boundary_column.present?
|
|
122
|
+
|
|
123
|
+
@print_boundary = true
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
sig { returns(T::Boolean) }
|
|
127
|
+
def print_boundary?
|
|
128
|
+
@print_boundary
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class << self
|
|
132
|
+
extend T::Sig
|
|
133
|
+
|
|
134
|
+
sig { params(number: Integer).returns(String) }
|
|
135
|
+
def base_26_string(number)
|
|
136
|
+
first_character = (number % 26 + 'A'.ord).chr
|
|
137
|
+
return first_character if number < 26
|
|
138
|
+
|
|
139
|
+
"#{base_26_string((number / 26).to_i - 1)}#{(number % 26 + 'A'.ord).chr}"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|