activerecord-bitemporal 1.0.0 → 2.0.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: 712d3f7bf1cb73ff98bbe7f5ca15f860c62096e419b89d44e6a419ba865eec38
4
+ data.tar.gz: 4071219bd39db37fc51dee4d1689f722c0a8c2ee6eb16120373d21ecf234def8
5
5
  SHA512:
6
- metadata.gz: 48b484ead6164bd5784a9060085267e7b6b9ae80a23f5015177ed45e4e82e702634731b2bb90a5ac67018c6d3052edf6839095327bc22bfbab98d412a013f279
7
- data.tar.gz: 004c5991bcebf4079b98665fb295d45cb473ebf2f83f0cdbf4ca08770003e7f4cf38f969b18e8d265fe2b39e3621ae5201953396dcecf9a79010f55c875be1b4
6
+ metadata.gz: fb0f10288f109171dd11aed5221fe0297224c6d798278ba60c18cad092cf4b757d2c5aff9463c9a9fafe0f1556f1d71d47d4d89d435f96f8debdda51883c0e42
7
+ data.tar.gz: 588eb1741174be2031b8e326fe34da93efebda700ad37bb26a260250a3e25d6b1ecb3ffc1964a7fb99c8ea4035d4a660e59ec617d5732320f944467a3d71b202
data/CHANGELOG.md CHANGED
@@ -1,39 +1,37 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 2.0.0
4
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.
5
+ ### Breaking Changed
6
+ - [[Proposal] Changed valid_in to exclude valid_from = to and valid_to = from. by osyo-manga · Pull Request #95](https://github.com/kufu/activerecord-bitemporal/pull/95)
9
7
 
10
8
  ### Added
11
9
 
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.
10
+ ### Changed
11
+ - [[Proposal] Add range argument to .valid_allin. by Dooor · Pull Request #98](https://github.com/kufu/activerecord-bitemporal/pull/98)
21
12
 
22
- | option | describe | default |
23
- | --- | --- | --- |
24
- | `enable_strict_by_validates_bitemporal_id` | raised with `validates :bitemporal_id, uniqueness: true` if `true` | false |
13
+ ### Deprecated
25
14
 
15
+ ### Removed
26
16
 
27
17
  ### Fixed
18
+ - [Fix JOIN query does not have valid_from / valid_to when using .or. by osyo-manga · Pull Request #99](https://github.com/kufu/activerecord-bitemporal/pull/99)
19
+ - [Fix typo in README.md by Naoya9922 · Pull Request #101](https://github.com/kufu/activerecord-bitemporal/pull/101)
20
+
21
+ ## 1.1.0
22
+
23
+ ### Added
28
24
 
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.
25
+ - [Add bitemporal data structure visualizer by wata727 · Pull Request #94](https://github.com/kufu/activerecord-bitemporal/pull/94)
26
+
27
+ ### Changed
36
28
 
37
29
  ### Deprecated
38
30
 
39
- - None
31
+ ### Removed
32
+
33
+ ### Fixed
34
+
35
+ ## 1.0.0
36
+
37
+ First stable release
data/README.md CHANGED
@@ -195,7 +195,7 @@ end
195
195
  | `valid_from` | `datetime` | 有効時間の開始時刻 |
196
196
  | `valid_to` | `datetime` | 有効時間の終了時刻 |
197
197
  | `transaction_from` | `datetime` | システム時間の開始時刻 |
198
- | `transaction_to` | `datetime` | システム時間の終了終了 |
198
+ | `transaction_to` | `datetime` | システム時間の終了時刻 |
199
199
 
200
200
  また、モデルクラスでは `ActiveRecord::Bitemporal` を `include` をする必要があります。
201
201
 
@@ -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.}
@@ -47,7 +47,7 @@ module ActiveRecord::Bitemporal
47
47
  end
48
48
  }
49
49
 
50
- def each_operatable_node(nodes = predicates, &block)
50
+ def each_operatable_node_6_0(nodes = predicates, &block)
51
51
  if block
52
52
  each_operatable_node(nodes).each(&block)
53
53
  else
@@ -56,11 +56,6 @@ module ActiveRecord::Bitemporal
56
56
  case node
57
57
  when Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual
58
58
  y << node if node && node.left.respond_to?(:relation)
59
- when Arel::Nodes::Or, Arel::Nodes::And
60
- if Gem::Version.new("6.1.0") <= ActiveRecord.version
61
- each_operatable_node(node.left) { |node| y << node }
62
- each_operatable_node(node.right) { |node| y << node }
63
- end
64
59
  when Arel::Nodes::Grouping
65
60
  each_operatable_node(node.expr) { |node| y << node }
66
61
  end
@@ -69,6 +64,36 @@ module ActiveRecord::Bitemporal
69
64
  end
70
65
  end
71
66
 
67
+ def each_operatable_node_6_1(nodes = predicates, &block)
68
+ if block
69
+ each_operatable_node_6_1(nodes).each(&block)
70
+ else
71
+ Enumerator.new { |y|
72
+ Array(nodes).each { |node|
73
+ case node
74
+ when Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual
75
+ y << node if node && node.left.respond_to?(:relation)
76
+ when Arel::Nodes::And
77
+ each_operatable_node_6_1(node.children) { |node| y << node }
78
+ when Arel::Nodes::Binary
79
+ each_operatable_node_6_1(node.left) { |node| y << node }
80
+ each_operatable_node_6_1(node.right) { |node| y << node }
81
+ when Arel::Nodes::Unary
82
+ each_operatable_node_6_1(node.expr) { |node| y << node }
83
+ end
84
+ }
85
+ }
86
+ end
87
+ end
88
+
89
+ def each_operatable_node(nodes = predicates, &block)
90
+ if Gem::Version.new("6.1.0") <= ActiveRecord.version
91
+ each_operatable_node_6_1(nodes, &block)
92
+ else
93
+ each_operatable_node_6_0(nodes, &block)
94
+ end
95
+ end
96
+
72
97
  def bitemporal_query_hash(*names)
73
98
  each_operatable_node
74
99
  .select { |node| names.include? node.left.name.to_s }
@@ -466,15 +491,53 @@ module ActiveRecord::Bitemporal
466
491
  where(bitemporal_id: id)
467
492
  }
468
493
 
469
- scope :valid_in, -> (from: nil, to: nil) {
470
- ignore_valid_datetime
471
- .tap { |relation| break relation.bitemporal_where_bind("valid_to", :gteq, from.in_time_zone.to_datetime) if from }
472
- .tap { |relation| break relation.bitemporal_where_bind("valid_from", :lteq, to.in_time_zone.to_datetime) if to }
494
+ # from < valid_to AND valid_from < to
495
+ scope :valid_in, -> (range = nil, from: nil, to: nil) {
496
+ return valid_in(from...to) if range.nil?
497
+
498
+ relation = ignore_valid_datetime
499
+ begin_, end_ = range.begin, range.end
500
+
501
+ # beginless range
502
+ if begin_
503
+ # from < valid_to
504
+ relation = relation.bitemporal_where_bind("valid_to", :gt, begin_.in_time_zone.to_datetime)
505
+ end
506
+
507
+ # endless range
508
+ if end_
509
+ if range.exclude_end?
510
+ # valid_from < to
511
+ relation = relation.bitemporal_where_bind("valid_from", :lt, end_.in_time_zone.to_datetime)
512
+ else
513
+ # valid_from <= to
514
+ relation = relation.bitemporal_where_bind("valid_from", :lteq, end_.in_time_zone.to_datetime)
515
+ end
516
+ end
517
+
518
+ relation
473
519
  }
474
- scope :valid_allin, -> (from: nil, to: nil) {
475
- ignore_valid_datetime
476
- .tap { |relation| break relation.bitemporal_where_bind("valid_from", :gteq, from.in_time_zone.to_datetime) if from }
477
- .tap { |relation| break relation.bitemporal_where_bind("valid_to", :lteq, to.in_time_zone.to_datetime) if to }
520
+
521
+ # from <= valid_from AND valid_to <= to
522
+ scope :valid_allin, -> (range = nil, from: nil, to: nil) {
523
+ return valid_allin(from..to) if range.nil?
524
+
525
+ relation = ignore_valid_datetime
526
+ begin_, end_ = range.begin, range.end
527
+
528
+ if begin_
529
+ relation = relation.bitemporal_where_bind("valid_from", :gteq, begin_.in_time_zone.to_datetime)
530
+ end
531
+
532
+ if end_
533
+ if range.exclude_end?
534
+ raise 'Range with excluding end is not supported'
535
+ else
536
+ relation = relation.bitemporal_where_bind("valid_to", :lteq, end_.in_time_zone.to_datetime)
537
+ end
538
+ end
539
+
540
+ relation
478
541
  }
479
542
  end
480
543
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Bitemporal
5
- VERSION = "1.0.0"
5
+ VERSION = "2.0.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: 2.0.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-09-16 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
@@ -187,7 +188,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
187
188
  - !ruby/object:Gem::Version
188
189
  version: '0'
189
190
  requirements: []
190
- rubygems_version: 3.3.3
191
+ rubygems_version: 3.3.7
191
192
  signing_key:
192
193
  specification_version: 4
193
194
  summary: BiTemporal Data Model for ActiveRecord