activerecord-bitemporal 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|