Class: Furud::Engine

Inherits:
Object
  • Object
show all
Defined in:
lib/furud/engine.rb,
sig/furud.rbs

Constant Summary collapse

ITERATION_FUNCTIONS =
%w[NOW TODAY RAND RANDBETWEEN OFFSET].freeze
ERROR_HANDLING_FUNCTIONS =
%w[IFERROR IFNA FILTER ISERR ISERROR ISNA ISBLANK ISNUMBER ISTEXT ISNONTEXT ISLOGICAL ISEVEN ISODD ISREF ISFORMULA ERROR.TYPE TYPE].freeze
REFERENCE_FUNCTIONS =
%w[ROW COLUMN ISREF ISFORMULA FORMULATEXT CELL OFFSET].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source = nil, functions: Functions.standard, locale: :en, iterative: false, max_iterations: 100, epsilon: 0.001) ⇒ Engine

Returns a new instance of Engine.

Parameters:

  • source (Object) (defaults to: nil)
  • functions: (Functions::Registry) (defaults to: Functions.standard)
  • locale: (Symbol) (defaults to: :en)
  • iterative: (Boolean) (defaults to: false)
  • max_iterations: (Integer) (defaults to: 100)
  • epsilon: (Numeric) (defaults to: 0.001)

Raises:

  • (ArgumentError)


13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/furud/engine.rb', line 13

def initialize(source = nil, functions: Functions.standard, locale: :en,
               iterative: false, max_iterations: 100, epsilon: 0.001)
  @source = source
  @functions = functions
  @locale = locale.to_sym
  @iterative = !!iterative
  @max_iterations = Integer(max_iterations)
  @epsilon = Float(epsilon)
  raise ArgumentError, "max_iterations must be positive" unless @max_iterations.positive?
  raise ArgumentError, "epsilon must be non-negative" if @epsilon.negative?

  @inputs = {}
  @formula_text = {}
  @formulas = {}
  @dynamic_formulas = Set.new
  @volatile_formulas = Set.new
  @evaluating = Set.new
  @values = {}
  @previous_values = {}
  @dirty = Set.new
  @precedents = {}
  @dependents = Hash.new { |hash, key| hash[key] = Set.new }
  @range_dependents = []
  @names = {}
  @cycles = []
  @spill_values = {}
  @spill_refs = Hash.new { |hash, key| hash[key] = Set.new }
  @spill_parents = {}
end

Instance Attribute Details

#locale ⇒ Symbol (readonly)

Returns the value of attribute locale.

Returns:

  • (Symbol)


11
12
13
# File 'lib/furud/engine.rb', line 11

def locale
  @locale
end

#source ⇒ Object (readonly)

Returns the value of attribute source.

Returns:

  • (Object)


11
12
13
# File 'lib/furud/engine.rb', line 11

def source
  @source
end

Instance Method Details

#clear(reference) ⇒ nil

Parameters:

Returns:

  • (nil)


62
63
64
65
66
67
68
69
70
71
72
# File 'lib/furud/engine.rb', line 62

def clear(reference)
  reference = coerce_reference(reference)
  @previous_values[reference] = @values[reference] unless @previous_values.key?(reference)
  remove_formula(reference)
  @inputs.delete(reference)
  @formula_text.delete(reference)
  @values.delete(reference)
  @dynamic_formulas.each { |cell| mark_dirty(cell) }
  mark_dirty(reference)
  nil
end

#cycles ⇒ Array[Array[Reference]]

Returns:



123
# File 'lib/furud/engine.rb', line 123

def cycles = @cycles.map(&:dup)

#define_name(name, area) ⇒ Reference, Area

Parameters:

Returns:



130
131
132
133
134
135
136
# File 'lib/furud/engine.rb', line 130

def define_name(name, area)
  normalized = normalize_name(name)
  @names[normalized] = area
  rebuild_formula_graph
  @formulas.each_key { |reference| mark_dirty(reference) }
  area
end

#delete_columns(sheet, at, count = 1) ⇒ self

Parameters:

  • sheet (String, nil)
  • at (Integer)
  • count (Integer) (defaults to: 1)

Returns:

  • (self)


128
# File 'lib/furud/engine.rb', line 128

def delete_columns(sheet, at, count = 1) = adjust_cells(:delete_columns, sheet, at, count)

#delete_rows(sheet, at, count = 1) ⇒ self

Parameters:

  • sheet (String, nil)
  • at (Integer)
  • count (Integer) (defaults to: 1)

Returns:

  • (self)


126
# File 'lib/furud/engine.rb', line 126

def delete_rows(sheet, at, count = 1) = adjust_cells(:delete_rows, sheet, at, count)

#dependents(reference) ⇒ Array[Reference]

Parameters:

Returns:



142
143
144
145
146
147
# File 'lib/furud/engine.rb', line 142

def dependents(reference)
  reference = coerce_reference(reference)
  direct = @dependents.fetch(reference, Set.new).to_a
  ranged = @range_dependents.filter_map { |area, owner| owner if area.include?(reference) }
  (direct + ranged).uniq.sort_by { |cell| sort_key(cell) }
end

#dirty ⇒ Set[Reference]

Returns:



122
# File 'lib/furud/engine.rb', line 122

def dirty = @dirty.dup

#formula(reference) ⇒ String?

Parameters:

Returns:

  • (String, nil)


83
84
85
# File 'lib/furud/engine.rb', line 83

def formula(reference)
  @formula_text[coerce_reference(reference)]
end

#insert_columns(sheet, at, count = 1) ⇒ self

Parameters:

  • sheet (String, nil)
  • at (Integer)
  • count (Integer) (defaults to: 1)

Returns:

  • (self)


127
# File 'lib/furud/engine.rb', line 127

def insert_columns(sheet, at, count = 1) = adjust_cells(:insert_columns, sheet, at, count)

#insert_rows(sheet, at, count = 1) ⇒ self

Parameters:

  • sheet (String, nil)
  • at (Integer)
  • count (Integer) (defaults to: 1)

Returns:

  • (self)


125
# File 'lib/furud/engine.rb', line 125

def insert_rows(sheet, at, count = 1) = adjust_cells(:insert_rows, sheet, at, count)

#precedents(reference) ⇒ Array[Reference | Area]

Parameters:

Returns:



138
139
140
# File 'lib/furud/engine.rb', line 138

def precedents(reference)
  @precedents.fetch(coerce_reference(reference), []).dup
end

#recalculate ⇒ Array[Reference]

Returns:



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/furud/engine.rb', line 87

def recalculate
  volatile_references.each { |reference| mark_dirty(reference) }
  return [] if @dirty.empty?

  pending = @dirty.dup
  @dirty.clear
  previous_spills = @spill_values.dup
  pending.each { |reference| @previous_values[reference] = @values[reference] unless @previous_values.key?(reference) }
  formula_order, cycle_groups = strongly_connected_components(pending)
  @cycles = @cycles.reject { |cycle| cycle.any? { |reference| pending.include?(reference) } } + cycle_groups
  cycle_members = cycle_groups.flatten.to_set

  pending.each { |reference| clear_spill(reference) }

  if @iterative
    cycle_groups.each { |cycle| iterate_cycle(cycle) }
  else
    cycle_members.each { |reference| store_value(reference, ErrorValue.new(code: :cycle)) }
  end

  formula_order.each do |reference|
    next if cycle_members.include?(reference)

    evaluate_cell(reference) unless @dirty.include?(reference)
  end
  pending.each { |reference| @previous_values.delete(reference) unless @values.key?(reference) }
  changed = pending.select do |reference|
    previous = @previous_values.delete(reference)
    previous != value_without_recalculation(reference)
  end
  spill_cells = previous_spills.keys | @spill_values.keys
  changed.concat(spill_cells.select { |reference| previous_spills[reference] != @spill_values[reference] })
  changed.uniq.sort_by { |reference| sort_key(reference) }
end

#set(reference, input) ⇒ Object

Parameters:

Returns:

  • (Object)


43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/furud/engine.rb', line 43

def set(reference, input)
  reference = coerce_reference(reference)
  ast = Formula.parse(input, origin: reference) if input.is_a?(String) && input.start_with?("=")
  @previous_values[reference] = @values[reference] unless @previous_values.key?(reference)
  remove_formula(reference)
  @inputs[reference] = input
  if ast
    @formulas[reference] = ast
    @formula_text[reference] = input
    add_formula_edges(reference, ast)
  else
    @formula_text.delete(reference)
    @values[reference] = input
  end
  @dynamic_formulas.each { |cell| mark_dirty(cell) }
  mark_dirty(reference)
  input
end

#value(reference) ⇒ Object

Parameters:

Returns:

  • (Object)


74
75
76
77
78
79
80
81
# File 'lib/furud/engine.rb', line 74

def value(reference)
  reference = coerce_reference(reference)
  recalculate unless @dirty.empty?
  return @spill_values[reference] if @spill_values.key?(reference)
  return @values[reference] if @values.key?(reference)

  source_value(reference)
end