Class: NTable::Structure

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/ntable/structure.rb

Overview

A Structure describes how a table is laid out: how many dimensions it has, how large the table is in each of those dimensions, what the axes are called, and how the coordinates are labeled/named. It is essentially an ordered list of named axes, along with some meta-information. A Structure is capable of performing computations such as determining how to look up data at a particular coordinate.

Generally, you create a new empty structure, and then use the #add method to define the axes. Provide the axis by creating an axis object (for example, an instance of IndexedAxis or LabeledAxis.) You can also optionally provide a name for the axis.

Once a Structure is used by a table, it is locked and cannot be modified further. However, a Structure can be shared by multiple tables.

Many table operations (such as slice) automatically compute the structure of the result.

Defined Under Namespace

Classes: AxisInfo, Position

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeStructure

Create an empty Structure. An empty structure corresponds to a table with no axes and a single value (i.e. a scalar). Generally, you should add axes using the Structure#add method before using the structure.



267
268
269
270
271
272
273
# File 'lib/ntable/structure.rb', line 267

def initialize
  @indexes = []
  @names = {}
  @size = 1
  @locked = false
  @parent = nil
end

Class Method Details

.add(axis_, name_ = nil) ⇒ Object

Create a new structure and automatically add the given axis. See Structure#add.



744
745
746
# File 'lib/ntable/structure.rb', line 744

def add(axis_, name_=nil)
  self.new.add(axis_, name_)
end

.from_json_array(array_) ⇒ Object

Deserialize a structure from the given JSON array



751
752
753
# File 'lib/ntable/structure.rb', line 751

def from_json_array(array_)
  self.new.from_json_array(array_)
end

Instance Method Details

#==(rhs_) ⇒ Object

Returns true if the two structures are equivalent in the axes but not necessarily in the offsets. The structure of a shared slice is equivalent, in this sense, to the “same” structure created from scratch, even though one is a subview and the other is not.



327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/ntable/structure.rb', line 327

def ==(rhs_)
  if rhs_.equal?(self)
    true
  elsif rhs_.is_a?(Structure)
    rhs_indexes_ = rhs_.instance_variable_get(:@indexes)
    if rhs_indexes_.size == @indexes.size
      rhs_indexes_.each_with_index do |rhs_ai_, i_|
        lhs_ai_ = @indexes[i_]
        return false unless lhs_ai_.axis_object == rhs_ai_.axis_object && lhs_ai_.axis_name == rhs_ai_.axis_name
      end
      return true
    end
    false
  else
    false
  end
end

#_compute_coords_for_vector(vector_) ⇒ Object

:nodoc:



691
692
693
694
695
# File 'lib/ntable/structure.rb', line 691

def _compute_coords_for_vector(vector_)  # :nodoc:
  vector_.map.with_index do |v_, i_|
    @indexes[i_].label(v_)
  end
end

#_compute_offset_for_vector(vector_) ⇒ Object

:nodoc:



682
683
684
685
686
687
688
# File 'lib/ntable/structure.rb', line 682

def _compute_offset_for_vector(vector_)  # :nodoc:
  offset_ = 0
  vector_.each_with_index do |v_, i_|
    offset_ += v_ * @indexes[i_].step
  end
  offset_
end

#_compute_position_coords(offset_) ⇒ Object

:nodoc:



728
729
730
731
732
733
734
735
# File 'lib/ntable/structure.rb', line 728

def _compute_position_coords(offset_)  # :nodoc:
  raise StructureStateError, "Structure not locked" unless @locked
  @indexes.map do |ainfo_|
    i_ = offset_ / ainfo_.step
    offset_ -= ainfo_.step * i_
    ainfo_.label(i_)
  end
end

#_dec_vector(vector_) ⇒ Object

:nodoc:



713
714
715
716
717
718
719
720
721
722
723
724
725
# File 'lib/ntable/structure.rb', line 713

def _dec_vector(vector_)  # :nodoc:
  (vector_.size - 1).downto(-1) do |i_|
    return true if i_ < 0
    v_ = vector_[i_] - 1
    if v_ < 0
      vector_[i_] = @indexes[i_].size - 1
    else
      vector_[i_] = v_
      break
    end
  end
  false
end

#_inc_vector(vector_) ⇒ Object

:nodoc:



698
699
700
701
702
703
704
705
706
707
708
709
710
# File 'lib/ntable/structure.rb', line 698

def _inc_vector(vector_)  # :nodoc:
  (vector_.size - 1).downto(-1) do |i_|
    return true if i_ < 0
    v_ = vector_[i_] + 1
    if v_ >= @indexes[i_].size
      vector_[i_] = 0
    else
      vector_[i_] = v_
      break
    end
  end
  false
end

#_offset(arg_) ⇒ Object

:nodoc:



621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
# File 'lib/ntable/structure.rb', line 621

def _offset(arg_)  # :nodoc:
  raise StructureStateError, "Structure not locked" unless @locked
  return nil unless @size > 0
  case arg_
  when ::Hash
    offset_ = 0
    arg_.each do |k_, v_|
      if (ainfo_ = axis(k_))
        delta_ = ainfo_._compute_offset(v_)
        return nil unless delta_
        offset_ += delta_
      else
        return nil
      end
    end
    offset_
  when ::Array
    offset_ = 0
    arg_.each_with_index do |v_, i_|
      if (ainfo_ = @indexes[i_])
        delta_ = ainfo_._compute_offset(v_)
        return nil unless delta_
        offset_ += delta_
      else
        return nil
      end
    end
    offset_
  else
    nil
  end
end

#_substructure(axes_, bool_) ⇒ Object

:nodoc:



598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
# File 'lib/ntable/structure.rb', line 598

def _substructure(axes_, bool_)  # :nodoc:
  raise StructureStateError, "Structure not locked" unless @locked
  sub_ = Structure.new
  indexes_ = []
  names_ = {}
  size_ = 1
  @indexes.each do |ainfo_|
    if axes_.include?(ainfo_.axis_index) == bool_
      nainfo_ = AxisInfo.new(ainfo_.axis_object, indexes_.size, ainfo_.axis_name, ainfo_.step)
      indexes_ << nainfo_
      names_[ainfo_.axis_name] = nainfo_
      size_ *= ainfo_.size
    end
  end
  sub_.instance_variable_set(:@indexes, indexes_)
  sub_.instance_variable_set(:@names, names_)
  sub_.instance_variable_set(:@size, size_)
  sub_.instance_variable_set(:@locked, true)
  sub_.instance_variable_set(:@parent, self)
  sub_
end

#_vector(arg_) ⇒ Object

:nodoc:



655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
# File 'lib/ntable/structure.rb', line 655

def _vector(arg_)  # :nodoc:
  raise StructureStateError, "Structure not locked" unless @locked
  return nil unless @size > 0
  vec_ = ::Array.new(@indexes.size, 0)
  case arg_
  when ::Hash
    arg_.each do |k_, v_|
      if (ainfo_ = axis(k_))
        val_ = ainfo_.index(v_)
        vec_[ainfo_.axis_index] = val_ if val_
      end
    end
    vec_
  when ::Array
    arg_.each_with_index do |v_, i_|
      if (ainfo_ = @indexes[i_])
        val_ = ainfo_.index(v_)
        vec_[i_] = val_ if val_
      end
    end
    vec_
  else
    nil
  end
end

#add(axis_, name_ = nil) ⇒ Object

Append an axis to the configuration of this structure. You must provide the axis, as an object that duck-types EmptyAxis. You may also provide an optional name string.



350
351
352
353
354
355
356
357
358
# File 'lib/ntable/structure.rb', line 350

def add(axis_, name_=nil)
  raise StructureStateError, "Structure locked" if @locked
  name_ = name_ ? name_.to_s : nil
  ainfo_ = AxisInfo.new(axis_, @indexes.size, name_)
  @indexes << ainfo_
  @names[name_] = ainfo_ if name_
  @size *= axis_.size
  self
end

#all_axesObject

Returns an array of AxisInfo objects representing all the axes of this structure.



437
438
439
# File 'lib/ntable/structure.rb', line 437

def all_axes
  @indexes.dup
end

#axis(axis_) ⇒ Object Also known as: []

Returns the AxisInfo object representing the given axis. The axis must be specified by 0-based index or by name string. Returns nil if there is no such axis.



446
447
448
449
450
451
452
453
# File 'lib/ntable/structure.rb', line 446

def axis(axis_)
  case axis_
  when ::Integer
    @indexes[axis_]
  else
    @names[axis_.to_s]
  end
end

#create(data_ = {}) ⇒ Object

Create a new table using this structure as the structure. Note that this also has the side effect of locking this structure.

You can initialize the data using the following options:

:fill

Fill all cells with the given value.

:load

Load the cell data with the values from the given array, in order.



593
594
595
# File 'lib/ntable/structure.rb', line 593

def create(data_={})
  Table.new(self, data_)
end

#degenerate?Boolean

Returns true if this is a degenerate/scalar structure. That is, if the dimension is 0.

Returns:

  • (Boolean)


429
430
431
# File 'lib/ntable/structure.rb', line 429

def degenerate?
  @indexes.size == 0
end

#dimObject

Returns the number of axes/dimensions currently in this structure.



421
422
423
# File 'lib/ntable/structure.rb', line 421

def dim
  @indexes.size
end

#each(&block_) ⇒ Object

Iterate over the axes in order, yielding AxisInfo objects.



459
460
461
# File 'lib/ntable/structure.rb', line 459

def each(&block_)
  @indexes.each(&block_)
end

#empty?Boolean

Returns true if this structure implies an “empty” table, one with no cells. This happens only if at least one of the axes has a zero size.

Returns:

  • (Boolean)


503
504
505
# File 'lib/ntable/structure.rb', line 503

def empty?
  @size == 0
end

#eql?(rhs_) ⇒ Boolean

Returns true if the two structures are equivalent, both in the axes and in the parentage. The structure of a shared slice is not equivalent, in this sense, to the “same” structure created from scratch, because the former is a subview of a larger structure whereas the latter is not.

Returns:

  • (Boolean)


314
315
316
317
318
319
# File 'lib/ntable/structure.rb', line 314

def eql?(rhs_)
  rhs_.equal?(self) ||
    rhs_.is_a?(Structure) &&
    @parent.eql?(rhs_.instance_variable_get(:@parent)) &&
    @indexes.eql?(rhs_.instance_variable_get(:@indexes))
end

#from_json_array(array_) ⇒ Object

Use the given array to reconstitute a structure previously serialized using Structure#to_json_array.



560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
# File 'lib/ntable/structure.rb', line 560

def from_json_array(array_)
  if @indexes.size > 0
    raise StructureStateError, "There are already axes in this structure"
  end
  array_.each do |obj_|
    name_ = obj_['name']
    type_ = obj_['type'] || 'Empty'
    if type_ =~ /^([a-z])(.*)$/
      mod_ = ::NTable.const_get("#{$1.upcase}#{$2}Axis")
    else
      mod_ = ::Kernel
      type_.split('::').each do |t_|
        mod_ = mod_.const_get(t_)
      end
    end
    axis_ = mod_.allocate
    axis_.from_json_object(obj_)
    add(axis_, name_)
  end
  self
end

#initialize_copy(other_) ⇒ Object

:nodoc:



276
277
278
279
280
281
282
283
284
285
286
# File 'lib/ntable/structure.rb', line 276

def initialize_copy(other_)  # :nodoc:
  initialize
  other_.instance_variable_get(:@indexes).each do |ai_|
    ai_ = ai_.dup
    @indexes << ai_
    if (name_ = ai_.axis_name)
      @names[name_] = ai_
    end
  end
  @size = other_.size
end

#inspectObject Also known as: to_s

Basic output.



301
302
303
304
# File 'lib/ntable/structure.rb', line 301

def inspect
  axes_ = @indexes.map{ |a_| "#{a_.axis_name}:#{a_.axis_object.class.name.sub('NTable::', '')}" }
  "#<#{self.class}:0x#{object_id.to_s(16)} #{axes_.join(', ')}#{@parent ? ' (sub)' : ''}>"
end

#lock!Object

Lock this structure, preventing further modification. Generally, this is done automatically when a structure is used by a table; you normally do not need to call it yourself.



470
471
472
473
474
475
476
477
478
479
480
481
482
# File 'lib/ntable/structure.rb', line 470

def lock!
  unless @locked
    @locked = true
    if @size > 0
      s_ = @size
      @indexes.each do |ainfo_|
        s_ /= ainfo_.size
        ainfo_._set_step(s_)
      end
    end
  end
  self
end

#locked?Boolean

Returns true if this structure has been locked.

Returns:

  • (Boolean)


487
488
489
# File 'lib/ntable/structure.rb', line 487

def locked?
  @locked
end

#parentObject

Returns the parent structure if this is a sub-view into a larger structure, or nil if not.



414
415
416
# File 'lib/ntable/structure.rb', line 414

def parent
  @parent
end

#position(arg_) ⇒ Object

Creates a Position object for the given argument. The argument may be a hash of row labels by axis name, or it may be an array of row labels for the axes in order.



512
513
514
515
# File 'lib/ntable/structure.rb', line 512

def position(arg_)
  vector_ = _vector(arg_)
  vector_ ? Position.new(self, vector_) : nil
end

#remove(axis_) ⇒ Object

Remove the given axis from the configuration. You may specify the axis by 0-based index, or by name string. Raises UnknownAxisError if there is no such axis.



365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/ntable/structure.rb', line 365

def remove(axis_)
  raise StructureStateError, "Structure locked" if @locked
  ainfo_ = axis(axis_)
  unless ainfo_
    raise UnknownAxisError, "Unknown axis: #{axis_.inspect}"
  end
  index_ = ainfo_.axis_index
  @names.delete(ainfo_.axis_name)
  @indexes.delete_at(index_)
  @indexes[index_..-1].each{ |ai_| ai_._dec_index }
  size_ = ainfo_.size
  if size_ == 0
    @size = @indexes.inject(1){ |s_, ai_| s_ * ai_.size }
  else
    @size /= size_
  end
  self
end

#replace(axis_, naxis_ = nil) ⇒ Object

Replace the given axis already in the configuration, with the given new axis. The old axis must be specified by 0-based index or by name string. The new axis must be provided as an axis object that duck-types EmptyAxis.

Raises UnknownAxisError if the given old axis specification does not match an actual axis.



393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'lib/ntable/structure.rb', line 393

def replace(axis_, naxis_=nil)
  raise StructureStateError, "Structure locked" if @locked
  ainfo_ = axis(axis_)
  unless ainfo_
    raise UnknownAxisError, "Unknown axis: #{axis_.inspect}"
  end
  osize_ = ainfo_.size
  naxis_ ||= yield(ainfo_)
  ainfo_._set_axis(naxis_)
  if osize_ == 0
    @size = @indexes.inject(1){ |size_, ai_| size_ * ai_.size }
  else
    @size = @size / osize_ * naxis_.size
  end
  self
end

#sizeObject

Returns the number of cells in a table with this structure.



494
495
496
# File 'lib/ntable/structure.rb', line 494

def size
  @size
end

#substructure_including(*axes_) ⇒ Object

Create a new substructure of this structure. The new structure has this structure as its parent, but includes only the given axes, which can be provided as an array of axis names or indexes.



522
523
524
# File 'lib/ntable/structure.rb', line 522

def substructure_including(*axes_)
  _substructure(axes_.flatten, true)
end

#substructure_omitting(*axes_) ⇒ Object

Create a new substructure of this structure. The new structure has this structure as its parent, but includes all axes EXCEPT the given axes, provided as an array of axis names or indexes.



531
532
533
# File 'lib/ntable/structure.rb', line 531

def substructure_omitting(*axes_)
  _substructure(axes_.flatten, false)
end

#to_json_arrayObject

Returns an array of objects representing the configuration of this structure. Such an array can be serialized as JSON, and used to replicate this structure using from_json_array.



540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
# File 'lib/ntable/structure.rb', line 540

def to_json_array
  @indexes.map do |ai_|
    name_ = ai_.axis_name
    axis_ = ai_.axis_object
    type_ = axis_.class.name
    if type_ =~ /^NTable::(\w+)Axis$/
      type_ = $1
      type_ = type_[0..0].downcase + type_[1..-1]
    end
    obj_ = {'type' => type_}
    obj_['name'] = name_ if name_
    axis_.to_json_object(obj_)
    obj_
  end
end

#unlocked_copyObject

Create an unlocked copy of this structure that can be further modified.



292
293
294
295
296
# File 'lib/ntable/structure.rb', line 292

def unlocked_copy
  copy_ = Structure.new
  @indexes.each{ |ai_| copy_.add(ai_.axis_object, ai_.axis_name) }
  copy_
end