class Soroban::Sheet

A container for cells. This is what the end user of Soroban will manipulate, either directly or via an importer that returns a Sheet instance.

Attributes

bindings[R]

Public Class Methods

new(logger=nil) click to toggle source

Creates a new sheet.

# File lib/soroban/sheet.rb, line 20
def initialize(logger=nil)
  @_logger = logger
  @_cells = {}
  @_changes = Hash.new { |h, k| h[k] = Set.new }
  @bindings = {}
end

Public Instance Methods

bind(options_hash) click to toggle source

Bind one or more named variables to a cell.

# File lib/soroban/sheet.rb, line 74
def bind(options_hash)
  options_hash.each do |name, label_or_range|
    _debug("binding '#{name}' to '#{label_or_range}'}")
    if Soroban::Helpers.range?(label_or_range)
      Soroban::LabelWalker.new(label_or_range).each do |label|
        next if @_cells.has_key?(label.to_sym)
        raise Soroban::UndefinedError, "Cannot bind '#{name}' to range '#{label_or_range}'; cell #{label} is not defined"
      end
      _bind_range(name, label_or_range)
    else
      unless @_cells.has_key?(label_or_range.to_sym)
        raise Soroban::UndefinedError, "Cannot bind '#{name}' to non-existent cell '#{label_or_range}'"
      end
      _bind(name, label_or_range)
    end
  end
end
cells() click to toggle source

Return a hash of `label => contents` for each cell in the sheet.

# File lib/soroban/sheet.rb, line 98
def cells
  labels = @_cells.keys.map(&:to_sym)
  contents = labels.map { |label| eval("@#{label}.excel") }
  Hash[labels.zip(contents)]
end
get(label_or_name) click to toggle source

Retrieve the contents of a cell.

# File lib/soroban/sheet.rb, line 63
def get(label_or_name)
  label = @bindings[label_or_name.to_sym] || label_or_name
  _debug("retrieving '#{label_or_name}' from '#{label}'}")
  if Soroban::Helpers.range?(label)
    walk(label)
  else
    _get(label_or_name, eval("@#{label}", binding))
  end
end
method_missing(method, *args, &block) click to toggle source

Used for calling dynamically defined functions, and for creating new cells (via `label=`).

Calls superclass method
# File lib/soroban/sheet.rb, line 29
def method_missing(method, *args, &block)
  if match = /^func_(.*)$/i.match(method.to_s)
    return Soroban::Functions.call(self, match[1], *args)
  elsif match = /^([a-z][\w]*)=$/i.match(method.to_s)
    return _add(match[1], args[0])
  end
  super
end
missing() click to toggle source

Return an array of referenced but undefined cells.

# File lib/soroban/sheet.rb, line 105
def missing
  (@_cells.values.reduce(:|) - @_cells.keys).to_a
end
set(options_hash) click to toggle source

Set the contents of one or more cells or ranges.

# File lib/soroban/sheet.rb, line 39
def set(options_hash)
  options_hash.each do |label_or_range, contents|
    _debug("setting '#{label_or_range}' to '#{contents}'")
    unless Soroban::Helpers.range?(label_or_range)
      _add(label_or_range, contents)
      next
    end
    fc, fr, tc, tr = Soroban::Helpers.getRange(label_or_range)
    if fc == tc || fr == tr
      raise ArgumentError, "Expecting an array when setting #{label_or_range}" unless contents.kind_of? Array
      cc, cr = fc, fr
      contents.each do |item|
        set("#{cc}#{cr}" => item)
        cc.next! if fr == tr
        cr.next! if fc == tc
      end
      raise Soroban::RangeError, "Supplied array doesn't match range length" if cc != tc && cr != tr
    else
      raise ArgumentError, "Can only set cells or 1-dimensional ranges of cells"
    end
  end
end
walk(range) click to toggle source

Visit each cell in the supplied range, yielding its value.

# File lib/soroban/sheet.rb, line 93
def walk(range)
  Soroban::ValueWalker.new(range, binding)
end

Private Instance Methods

_add(label, contents) click to toggle source
# File lib/soroban/sheet.rb, line 124
def _add(label, contents)
  name = @bindings[label.to_sym] || label
  if cells.has_key?(name)
    cell = eval("@#{name}", binding)
    cell.set(contents)
    return
  end
  internal = "@#{label}"
  _expose(internal, label)
  cell = Soroban::Cell.new(binding)
  _set(label, cell, contents)
  instance_variable_set(internal, cell)
end
_bind(name, label) click to toggle source
# File lib/soroban/sheet.rb, line 167
def _bind(name, label)
  @bindings[name.to_sym] = label.to_sym
  internal = "@#{label}"
  _expose(internal, name)
end
_bind_range(name, range) click to toggle source
# File lib/soroban/sheet.rb, line 173
    def _bind_range(name, range)
      @bindings[name.to_sym] = range.to_s
      instance_eval <<-EOV, __FILE__, __LINE__ + 1
        def #{name}
          walk("#{range}")
        end
      EOV
    end
_clear(name) click to toggle source
# File lib/soroban/sheet.rb, line 148
def _clear(name)
  @_changes[name].each do |target|
    next unless @_cells.has_key?(target)
    begin
      eval("@#{target.to_s}.clear")
      _clear(target)
    rescue
    end
  end
end
_debug(message) click to toggle source
# File lib/soroban/sheet.rb, line 111
def _debug(message)
  return if @_logger.nil?
  @_logger.debug "SOROBAN: #{message}"
end
_expose(internal, name) click to toggle source
# File lib/soroban/sheet.rb, line 182
    def _expose(internal, name)
      instance_eval <<-EOV, __FILE__, __LINE__ + 1
        def #{name}
          _get("#{name}", #{internal})
        end
        def #{name}=(contents)
          _set("#{name}", #{internal}, contents)
        end
      EOV
    end
_get(label_or_name, cell) click to toggle source
# File lib/soroban/sheet.rb, line 159
def _get(label_or_name, cell)
  label = label_or_name.to_sym
  name = @bindings[label] || label
  badref = @_cells[name] & missing
  raise Soroban::UndefinedError, "Unmet dependencies #{badref.to_a.join(', ')} for #{label}" if badref.length > 0
  cell.get
end
_set(label_or_name, cell, contents) click to toggle source
# File lib/soroban/sheet.rb, line 138
def _set(label_or_name, cell, contents)
  label = label_or_name.to_sym
  name = @bindings[label] || label
  _unlink(name, cell.dependencies)
  cell.set(contents)
  @_cells[name] = cell.dependencies
  _link(name, cell.dependencies)
  _clear(name)
end