activerecord-bitemporal 1.0.0 → 1.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3801f30411bb5e26d32aa1cb27ed45e5c12ebea99b688fcc27b9be1289099fc
4
- data.tar.gz: a1ef742f3e2b7c8cb3bd280ba0fb92a5f1f45b2504fc29dc7b789d2694c2b284
3
+ metadata.gz: 939636118c751abd0690d9956750820ffcbb970fef7e4c9b6ab29642d461bf79
4
+ data.tar.gz: caf2c137271a7c635318384b42c771d215849fb1ece08de062a0170f014ad0b4
5
5
  SHA512:
6
- metadata.gz: 48b484ead6164bd5784a9060085267e7b6b9ae80a23f5015177ed45e4e82e702634731b2bb90a5ac67018c6d3052edf6839095327bc22bfbab98d412a013f279
7
- data.tar.gz: 004c5991bcebf4079b98665fb295d45cb473ebf2f83f0cdbf4ca08770003e7f4cf38f969b18e8d265fe2b39e3621ae5201953396dcecf9a79010f55c875be1b4
6
+ metadata.gz: '05108b579eb4585ef4835677902933167209be9a7ed13482122f088c5b5a85ea528260b74ed619f3fcbbbb0d2ae85f711a2c06803b2c60d8821b88b9528fa914'
7
+ data.tar.gz: e38f7f394438418cbab0a9f237ef4352cd0b2e6e07dff1fbbd4be7cda37c318ff2ce85a2c332e92e015d47980b06e49e74ce76e3c0a4b232d25bb869a092b604
data/CHANGELOG.md CHANGED
@@ -1,39 +1,19 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
4
-
5
- ### Breaking Changes
6
-
7
- - [#15](https://github.com/kufu/activerecord-bitemporal/pull/15) - `validates :bitemporal_id, uniqueness: true` is no raise by default.
8
- - [#19](https://github.com/kufu/activerecord-bitemporal/pull/19) - Fix create history records after logical destroy in #destroy.
3
+ ## 1.1.0
9
4
 
10
5
  ### Added
11
6
 
12
- - [#12](https://github.com/kufu/activerecord-bitemporal/pull/12) - Added utility (and extension) scopes.
13
- - `.bitemporl_for(id)`
14
- - `.valid_in(from: from, to: to)`
15
- - `.valid_allin(from: from, to: to)`
16
- - `.bitemporal_histories_by(id)`
17
- - `.bitemporal_most_future(id)`
18
- - `.bitemporal_most_past(id)`
19
- - [#15](https://github.com/kufu/activerecord-bitemporal/pull/15) - Added `.bitemporalize`. Use `.bitemporalize` instead of `include ActiveRecord::Bitemporal`.
20
- - [#15](https://github.com/kufu/activerecord-bitemporal/pull/15) - Added `.bitemporalize` options.
7
+ - [Add bitemporal data structure visualizer by wata727 · Pull Request #94](https://github.com/kufu/activerecord-bitemporal/pull/94)
8
+
9
+ ### Changed
21
10
 
22
- | option | describe | default |
23
- | --- | --- | --- |
24
- | `enable_strict_by_validates_bitemporal_id` | raised with `validates :bitemporal_id, uniqueness: true` if `true` | false |
11
+ ### Deprecated
25
12
 
13
+ ### Removed
26
14
 
27
15
  ### Fixed
28
16
 
29
- - [#17](https://github.com/kufu/activerecord-bitemporal/pull/17) - Fixed bug in create record with valid_datetime out of the range valid_from to valid_to.
30
- - [#18](https://github.com/kufu/activerecord-bitemporal/pull/18) - `record.valid_datetime` is not nil when after `Model.valid_at("2019/1/1").ignore_valid_datetime`.
31
- - [#18](https://github.com/kufu/activerecord-bitemporal/pull/18) - `ignore_valid_datetime` is not applied in `ActiveRecord::Bitemporal.valid_at!`.
32
- - [#21](https://github.com/kufu/activerecord-bitemporal/pull/21) - Fixed bug in multi thread with `#update`.
33
- - [#24](https://github.com/kufu/activerecord-bitemporal/pull/24) [#25](https://github.com/kufu/activerecord-bitemporal/pull/25) - Fixed bug. Does not respect table alias on join clause.
34
- - [#27](https://github.com/kufu/activerecord-bitemporal/pull/27) - Fixed a bug that `swapped_id` doesn't change after `#reload`.
35
- - [#28](https://github.com/kufu/activerecord-bitemporal/pull/28) - Fix the bug that `valid_from == valid_to` record is generated.
36
-
37
- ### Deprecated
17
+ ## 1.0.0
38
18
 
39
- - None
19
+ First stable release
@@ -7,8 +7,8 @@ require "activerecord-bitemporal/version"
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = "activerecord-bitemporal"
9
9
  spec.version = ActiveRecord::Bitemporal::VERSION
10
- spec.authors = ["mserizawa"]
11
- spec.email = ["serizawa@smarthr.co.jp"]
10
+ spec.authors = ["SmartHR"]
11
+ spec.email = ["oss@smarthr.co.jp"]
12
12
 
13
13
  spec.summary = "BiTemporal Data Model for ActiveRecord"
14
14
  spec.description = %q{Enable ActiveRecord models to be handled as BiTemporal Data Model.}
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Bitemporal
5
- VERSION = "1.0.0"
5
+ VERSION = "1.1.0"
6
6
  end
7
7
  end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord::Bitemporal
4
+ module Visualizer
5
+ # Figure is a two-dimensional array holding plotted lines and columns
6
+ class Figure < Array
7
+ def print(str, line: 0, column: 0)
8
+ self[line] ||= []
9
+ str.each_char.with_index(column) do |c, i|
10
+ # The `#` represents a zero-length rectangle and should not be overwritten with lines
11
+ next if self[line][i] == '#' && (c == '+' || c == '|' || c == '-')
12
+
13
+ self[line][i] = c
14
+ end
15
+ end
16
+
17
+ def to_s
18
+ map { |l| l&.map { |c| c || ' ' }&.join }.join("\n")
19
+ end
20
+ end
21
+
22
+ module_function
23
+
24
+ def visualize(record, height: 10, width: 40, highlight: true)
25
+ histories = record.class.ignore_bitemporal_datetime.bitemporal_for(record).order(:transaction_from, :valid_from)
26
+
27
+ if highlight
28
+ visualize_records(histories, [record], height: height, width: width)
29
+ else
30
+ visualize_records(histories, height: height, width: width)
31
+ end
32
+ end
33
+
34
+ # e.g. visualize_records(ActiveRecord::Relation, ActiveRecord::Relation)
35
+ def visualize_records(*relations, height: 10, width: 40)
36
+ raise 'More than 3 relations are not supported' if relations.size >= 3
37
+ records = relations.flatten
38
+
39
+ valid_times = (records.map(&:valid_from) + records.map(&:valid_to)).sort.uniq
40
+ transaction_times = (records.map(&:transaction_from) + records.map(&:transaction_to)).sort.uniq
41
+
42
+ time_length = Time.zone.now.strftime('%F %T.%3N').length
43
+
44
+ columns = compute_positions(valid_times, length: width, left_margin: time_length + 1, outlier: ActiveRecord::Bitemporal::DEFAULT_VALID_TO)
45
+ lines = compute_positions(transaction_times, length: height, outlier: ActiveRecord::Bitemporal::DEFAULT_TRANSACTION_TO)
46
+
47
+ headers = Figure.new
48
+ valid_times.each_with_object([]).with_index do |(valid_time, prev_valid_times), line|
49
+ prev_valid_times.each do |valid_time|
50
+ headers.print('|', line: line, column: columns[valid_time])
51
+ end
52
+ headers.print("| #{valid_time.strftime('%F %T.%3N')}", line: line, column: columns[valid_time])
53
+ prev_valid_times << valid_time
54
+ end
55
+
56
+ body = Figure.new
57
+ relations.each.with_index do |relation, idx|
58
+ filler = idx == 0 ? ' ' : '*'
59
+
60
+ relation.each do |record|
61
+ line = lines[record.transaction_from]
62
+ column = columns[record.valid_from]
63
+
64
+ width = columns[record.valid_to] - columns[record.valid_from] - 1
65
+ height = lines[record.transaction_to] - lines[record.transaction_from] - 1
66
+
67
+ body.print("#{record.transaction_from.strftime('%F %T.%3N')} ", line: line)
68
+ if width > 0
69
+ if height > 0
70
+ body.print('+' + '-' * width + '+', line: line, column: column)
71
+ else
72
+ body.print('|' + '#' * width + '|', line: line, column: column)
73
+ end
74
+ else
75
+ body.print('#', line: line, column: column)
76
+ end
77
+
78
+ 1.upto(height) do |i|
79
+ if width > 0
80
+ body.print('|' + filler * width + '|', line: line + i, column: column)
81
+ else
82
+ body.print('#', line: line + i, column: column)
83
+ end
84
+ end
85
+
86
+ body.print("#{record.transaction_to.strftime('%F %T.%3N')} ", line: line + height + 1)
87
+ if width > 0
88
+ body.print('+' + '-' * width + '+', line: line + height + 1, column: column)
89
+ else
90
+ body.print('#', line: line + height + 1, column: column)
91
+ end
92
+ end
93
+ end
94
+
95
+ "#{headers.to_s}\n#{body.to_s}"
96
+ end
97
+
98
+ # Compute a dictionary of where each time should be plotted.
99
+ # The position is normalized to the actual length of time.
100
+ #
101
+ # Example:
102
+ #
103
+ # t1 t2 t3 t4
104
+ # |------|------|-----------------|
105
+ #
106
+ # f(t1, t2, t3, t4) -> { t1 => 0, t2 => 2, t3 => 4, t4 => 10 }
107
+ #
108
+ def compute_positions(times, length:, left_margin: 0, outlier: nil)
109
+ lengths_from_beginning = compute_lengths_from_beginning(times, outlier: outlier)
110
+ # times must be sorted in ascending order. This is caller's responsibility.
111
+ # In that case, the last of lengths_from_beginning is equal to the total length.
112
+ total = lengths_from_beginning.values.last
113
+
114
+ times.each_with_object({}) do |time, ret|
115
+ prev = ret.values.last
116
+ pos = (lengths_from_beginning[time] / total * length).to_i + left_margin
117
+
118
+ if prev
119
+ # If the difference of times is too short, a position that have already been plotted may be computed.
120
+ # But we still want to plot the time, so allocate the required number to plot the smallest area.
121
+ if pos <= prev
122
+ # | -> |*|
123
+ # ^^ 2 columns
124
+ pos = prev + 2
125
+ elsif pos == prev + 1
126
+ # || -> |*|
127
+ # ^ 1 column
128
+ pos += 1
129
+ end
130
+ end
131
+ ret[time] = pos
132
+ end
133
+ end
134
+
135
+ # Example:
136
+ #
137
+ # t1 t2 t3 t4
138
+ # |-----------|-----------|-----------|
139
+ # <-----------> l1
140
+ # <-----------------------> l2
141
+ # <-----------------------------------> l3
142
+ #
143
+ # f([t1, t2, t3, t4]) -> { t1 => 0, t2 => l1, t3 => l2, t4 => l3 }
144
+ #
145
+ def compute_lengths_from_beginning(times, outlier: nil)
146
+ times.each_with_object({}) do |time, ret|
147
+ ret[time] = if time == outlier && times.size > 2
148
+ # If it contains an extremely large value such as 9999-12-31,
149
+ # that point will have a large effect on the visualization,
150
+ # so adjust the length so that it is half of the whole.
151
+ ret.values.last * 2
152
+ else
153
+ time - times.min
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -6,6 +6,7 @@ require "activerecord-bitemporal/bitemporal"
6
6
  require "activerecord-bitemporal/scope"
7
7
  require "activerecord-bitemporal/patches"
8
8
  require "activerecord-bitemporal/version"
9
+ require "activerecord-bitemporal/visualizer"
9
10
 
10
11
  module ActiveRecord::Bitemporal
11
12
  DEFAULT_VALID_FROM = Time.utc(1900, 12, 31).in_time_zone.freeze
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-bitemporal
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
- - mserizawa
7
+ - SmartHR
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-05-12 00:00:00.000000000 Z
11
+ date: 2022-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -138,7 +138,7 @@ dependencies:
138
138
  version: '0'
139
139
  description: Enable ActiveRecord models to be handled as BiTemporal Data Model.
140
140
  email:
141
- - serizawa@smarthr.co.jp
141
+ - oss@smarthr.co.jp
142
142
  executables: []
143
143
  extensions: []
144
144
  extra_rdoc_files: []
@@ -168,6 +168,7 @@ files:
168
168
  - lib/activerecord-bitemporal/patches.rb
169
169
  - lib/activerecord-bitemporal/scope.rb
170
170
  - lib/activerecord-bitemporal/version.rb
171
+ - lib/activerecord-bitemporal/visualizer.rb
171
172
  homepage: https://github.com/kufu/activerecord-bitemporal
172
173
  licenses:
173
174
  - Apache 2.0