activerecord-bitemporal 1.0.0 → 2.0.0

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