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 +4 -4
- data/CHANGELOG.md +8 -28
- data/activerecord-bitemporal.gemspec +2 -2
- data/lib/activerecord-bitemporal/version.rb +1 -1
- data/lib/activerecord-bitemporal/visualizer.rb +158 -0
- data/lib/activerecord-bitemporal.rb +1 -0
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 939636118c751abd0690d9956750820ffcbb970fef7e4c9b6ab29642d461bf79
|
4
|
+
data.tar.gz: caf2c137271a7c635318384b42c771d215849fb1ece08de062a0170f014ad0b4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '05108b579eb4585ef4835677902933167209be9a7ed13482122f088c5b5a85ea528260b74ed619f3fcbbbb0d2ae85f711a2c06803b2c60d8821b88b9528fa914'
|
7
|
+
data.tar.gz: e38f7f394438418cbab0a9f237ef4352cd0b2e6e07dff1fbbd4be7cda37c318ff2ce85a2c332e92e015d47980b06e49e74ce76e3c0a4b232d25bb869a092b604
|
data/CHANGELOG.md
CHANGED
@@ -1,39 +1,19 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
##
|
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
|
-
- [#
|
13
|
-
|
14
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 = ["
|
11
|
-
spec.email = ["
|
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.}
|
@@ -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.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- SmartHR
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
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
|
-
-
|
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
|