Module: ActiveRecord::Bitemporal::Visualizer

Defined in:
lib/activerecord-bitemporal/visualizer.rb

Defined Under Namespace

Classes: Figure

Class Method Summary collapse

Class Method Details

.compute_lengths_from_beginning(times, outlier: nil) ⇒ Object

Example:

t1          t2          t3          t4
|-----------|-----------|-----------|
<-----------> l1
<-----------------------> l2
<-----------------------------------> l3

f([t1, t2, t3, t4]) -> { t1 => 0, t2 => l1, t3 => l2, t4 => l3 }


158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/activerecord-bitemporal/visualizer.rb', line 158

def compute_lengths_from_beginning(times, outlier: nil)
  times.each_with_object({}) do |time, ret|
    ret[time] = if time == outlier && times.size > 2
                  # If it contains an extremely large value such as 9999-12-31,
                  # that point will have a large effect on the visualization,
                  # so adjust the length so that it is half of the whole.
                  ret.values.last * 2
                else
                  time - times.min
                end
  end
end

.compute_positions(times, length:, left_margin: 0, outlier: nil) ⇒ Object

Compute a dictionary of where each time should be plotted. The position is normalized to the actual length of time.

Example:

t1     t2     t3                t4
|------|------|-----------------|

f(t1, t2, t3, t4) -> { t1 => 0, t2 => 2, t3 => 4, t4 => 10 }


121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/activerecord-bitemporal/visualizer.rb', line 121

def compute_positions(times, length:, left_margin: 0, outlier: nil)
  lengths_from_beginning = compute_lengths_from_beginning(times, outlier: outlier)
  # times must be sorted in ascending order. This is caller's responsibility.
  # In that case, the last of lengths_from_beginning is equal to the total length.
  total = lengths_from_beginning.values.last
  
  times.each_with_object({}) do |time, ret|
    prev = ret.values.last
    pos = (lengths_from_beginning[time] / total * length).to_i + left_margin
  
    if prev
      # If the difference of times is too short, a position that have already been plotted may be computed.
      # But we still want to plot the time, so allocate the required number to plot the smallest area.
      if pos <= prev
        # | -> |*|
        #       ^^ 2 columns
        pos = prev + 2
      elsif pos == prev + 1
        # || -> |*|
        #        ^ 1 column
        pos += 1
      end
    end
    ret[time] = pos
  end
end

.visualize(record, height: 10, width: 40, highlight: true) ⇒ Object



24
25
26
27
28
29
30
31
32
# File 'lib/activerecord-bitemporal/visualizer.rb', line 24

def visualize(record, height: 10, width: 40, highlight: true)
  histories = record.class.ignore_bitemporal_datetime.bitemporal_for(record).order(:transaction_from, record.valid_from_key)

  if highlight
    visualize_records(histories, [record], height: height, width: width)
  else
    visualize_records(histories, height: height, width: width)
  end
end

.visualize_records(*relations, height: 10, width: 40) ⇒ Object

e.g. visualize_records(ActiveRecord::Relation, ActiveRecord::Relation)



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/activerecord-bitemporal/visualizer.rb', line 35

def visualize_records(*relations, height: 10, width: 40)
  raise 'More than 3 relations are not supported' if relations.size >= 3
  records = relations.flatten

  valid_times = (records.map { _1[_1.valid_from_key] } + records.map { _1[_1.valid_to_key] }).sort.uniq
  transaction_times = (records.map(&:transaction_from) + records.map(&:transaction_to)).sort.uniq
  
  time_length = Time.zone.now.strftime('%F %T.%3N').length
  
  columns = compute_positions(valid_times, length: width, left_margin: time_length + 1, outlier: ActiveRecord::Bitemporal::DEFAULT_VALID_TO)
  lines = compute_positions(transaction_times, length: height, outlier: ActiveRecord::Bitemporal::DEFAULT_TRANSACTION_TO)
  
  headers = Figure.new
  valid_times.each_with_object([]).with_index do |(valid_time, prev_valid_times), line|
    prev_valid_times.each do |valid_time|
      headers.print('|', line: line, column: columns[valid_time])
    end
    valid_time_str = valid_time.kind_of?(Date) ? valid_time.strftime('%F') : valid_time.strftime('%F %T.%3N')
    headers.print("| #{valid_time_str}", line: line, column: columns[valid_time])
    prev_valid_times << valid_time
  end
  
  body = Figure.new
  relations.each.with_index do |relation, idx|
    filler = idx == 0 ? ' ' : '*'

    relation.each do |record|
      line = lines[record.transaction_from]
      valid_from = record[record.valid_from_key]
      valid_to = record[record.valid_to_key]
      column = columns[valid_from]

      width = columns[valid_to] - columns[valid_from] - 1
      height = lines[record.transaction_to] - lines[record.transaction_from] - 1

      body.print("#{record.transaction_from.strftime('%F %T.%3N')} ", line: line)
      if width > 0
        if height > 0
          body.print('+' + '-' * width + '+', line: line, column: column)
        else
          body.print('|' + '#' * width + '|', line: line, column: column)
        end
      else
        body.print('#', line: line, column: column)
      end

      1.upto(height) do |i|
        if width > 0
          body.print('|' + filler * width + '|', line: line + i, column: column)
        else
          body.print('#', line: line + i, column: column)
        end
      end

      body.print("#{record.transaction_to.strftime('%F %T.%3N')} ", line: line + height + 1)
      if width > 0
        body.print('+' + '-' * width + '+', line: line + height + 1, column: column)
      else
        body.print('#', line: line + height + 1, column: column)
      end
    end
  end

  valid_label = valid_times[0].kind_of?(Date) ? 'valid_date' : 'valid_datetime'
  transaction_label = 'transaction_datetime'
  right_margin = time_length + 1 - transaction_label.size

  label = if right_margin >= 0
    "#{transaction_label + ' ' * right_margin}| #{valid_label}"
  else
    "#{transaction_label[0...right_margin]}| #{valid_label}"
  end

  "#{label}\n#{headers.to_s}\n#{body.to_s}"
end