by_star 2.2.0 → 4.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.
Files changed (66) hide show
  1. checksums.yaml +5 -13
  2. data/.github/workflows/mysql.yml +92 -0
  3. data/.github/workflows/postgresql.yml +99 -0
  4. data/.gitignore +6 -5
  5. data/.travis.yml +92 -35
  6. data/CHANGELOG.md +59 -29
  7. data/Gemfile +18 -25
  8. data/MIT-LICENSE +20 -20
  9. data/README.md +616 -523
  10. data/Rakefile +18 -18
  11. data/UPGRADING +4 -12
  12. data/by_star.gemspec +34 -32
  13. data/cleaner.rb +24 -24
  14. data/lib/by_star/base.rb +69 -68
  15. data/lib/by_star/between.rb +185 -120
  16. data/lib/by_star/directional.rb +35 -21
  17. data/lib/by_star/kernel/date.rb +41 -0
  18. data/lib/by_star/kernel/in_time_zone.rb +20 -0
  19. data/lib/by_star/{kernel.rb → kernel/time.rb} +41 -41
  20. data/lib/by_star/normalization.rb +156 -118
  21. data/lib/by_star/orm/active_record/by_star.rb +75 -59
  22. data/lib/by_star/orm/mongoid/by_star.rb +90 -63
  23. data/lib/by_star/orm/mongoid/reorder.rb +23 -0
  24. data/lib/by_star/version.rb +3 -3
  25. data/lib/by_star.rb +18 -15
  26. data/spec/database.yml +15 -15
  27. data/spec/fixtures/active_record/models.rb +12 -10
  28. data/spec/fixtures/active_record/schema.rb +19 -19
  29. data/spec/fixtures/mongoid/models.rb +31 -29
  30. data/spec/fixtures/shared/seeds.rb +36 -26
  31. data/spec/gemfiles/Gemfile.rails +5 -0
  32. data/spec/gemfiles/Gemfile.rails32 +7 -0
  33. data/spec/gemfiles/Gemfile.rails40 +7 -0
  34. data/spec/gemfiles/Gemfile.rails41 +7 -0
  35. data/spec/gemfiles/Gemfile.rails42 +7 -0
  36. data/spec/gemfiles/Gemfile.rails50 +7 -0
  37. data/spec/gemfiles/Gemfile.rails51 +7 -0
  38. data/spec/gemfiles/Gemfile.rails52 +7 -0
  39. data/spec/gemfiles/Gemfile.rails60 +7 -0
  40. data/spec/gemfiles/Gemfile.rails61 +7 -0
  41. data/spec/integration/active_record/active_record_spec.rb +41 -53
  42. data/spec/integration/mongoid/mongoid_spec.rb +39 -46
  43. data/spec/integration/shared/at_time.rb +53 -0
  44. data/spec/integration/shared/between_dates.rb +99 -0
  45. data/spec/integration/shared/between_times.rb +99 -0
  46. data/spec/integration/shared/by_calendar_month.rb +55 -55
  47. data/spec/integration/shared/by_cweek.rb +54 -0
  48. data/spec/integration/shared/by_day.rb +120 -108
  49. data/spec/integration/shared/by_direction.rb +126 -114
  50. data/spec/integration/shared/by_fortnight.rb +48 -48
  51. data/spec/integration/shared/by_month.rb +50 -50
  52. data/spec/integration/shared/by_quarter.rb +49 -49
  53. data/spec/integration/shared/by_week.rb +54 -54
  54. data/spec/integration/shared/by_weekend.rb +49 -49
  55. data/spec/integration/shared/by_year.rb +48 -48
  56. data/spec/integration/shared/index_scope_parameter.rb +111 -0
  57. data/spec/integration/shared/offset_parameter.rb +32 -31
  58. data/spec/integration/shared/order_parameter.rb +36 -0
  59. data/spec/integration/shared/relative.rb +174 -174
  60. data/spec/spec_helper.rb +33 -29
  61. data/spec/unit/kernel_date_spec.rb +113 -0
  62. data/spec/unit/kernel_time_spec.rb +57 -57
  63. data/spec/unit/normalization_spec.rb +384 -255
  64. data/tmp/.gitignore +1 -1
  65. metadata +82 -68
  66. data/spec/integration/shared/scope_parameter.rb +0 -42
@@ -1,21 +1,35 @@
1
- module ByStar
2
-
3
- module Directional
4
-
5
- def before(*args)
6
- with_by_star_options(*args) do |time, options|
7
- time = ByStar::Normalization.time(time)
8
- before_query(time, options)
9
- end
10
- end
11
- alias_method :before_now, :before
12
-
13
- def after(*args)
14
- with_by_star_options(*args) do |time, options|
15
- time = ByStar::Normalization.time(time)
16
- after_query(time, options)
17
- end
18
- end
19
- alias_method :after_now, :after
20
- end
21
- end
1
+ module ByStar
2
+
3
+ module Directional
4
+
5
+ def before(*args)
6
+ with_by_star_options(*args) do |time, options|
7
+ field = by_star_start_field(options)
8
+ time = ByStar::Normalization.time(time)
9
+ by_star_before_query(self, field, time)
10
+ end
11
+ end
12
+ alias_method :before_now, :before
13
+
14
+ def after(*args)
15
+ with_by_star_options(*args) do |time, options|
16
+ field = by_star_start_field(options)
17
+ time = ByStar::Normalization.time(time)
18
+ by_star_after_query(self, field, time)
19
+ end
20
+ end
21
+ alias_method :after_now, :after
22
+
23
+ def oldest(*args)
24
+ with_by_star_options(*args) do |time, options|
25
+ oldest_query(options)
26
+ end
27
+ end
28
+
29
+ def newest(*args)
30
+ with_by_star_options(*args) do |time, options|
31
+ newest_query(options)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,41 @@
1
+ module ByStar
2
+
3
+ module Kernel
4
+
5
+ module Date
6
+
7
+ # A "Weekend" is defined as beginning of Saturday to end of Sunday.
8
+ # The weekend for a given date will be the the next weekend if the day Mon-Thurs,
9
+ # otherwise the current weekend if the day is Fri-Sun.
10
+ def beginning_of_weekend
11
+ beginning_of_week(:monday).advance(days: 5)
12
+ end
13
+
14
+ def end_of_weekend
15
+ beginning_of_weekend + 1
16
+ end
17
+
18
+ # A "Fortnight" is defined as a two week period, with the first fortnight of the
19
+ # year beginning on 1st January.
20
+ def beginning_of_fortnight
21
+ beginning_of_year + 14 * ((self - beginning_of_year) / 14).to_i
22
+ end
23
+
24
+ def end_of_fortnight
25
+ beginning_of_fortnight + 13
26
+ end
27
+
28
+ # A "Calendar Month" is defined as a month as it appears on a calendar, including days form
29
+ # previous/following months which are part of the first/last weeks of the given month.
30
+ def beginning_of_calendar_month(*args)
31
+ beginning_of_month.beginning_of_week(*args)
32
+ end
33
+
34
+ def end_of_calendar_month(*args)
35
+ end_of_month.end_of_week(*args)
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ ::Date.__send__(:include, ByStar::Kernel::Date)
@@ -0,0 +1,20 @@
1
+ module ByStar
2
+
3
+ module Kernel
4
+
5
+ module InTimeZone
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ if method_defined?(:to_time_in_current_zone) && !method_defined?(:in_time_zone)
10
+ alias_method :in_time_zone, :to_time_in_current_zone
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ ::Date.__send__(:include, ByStar::Kernel::InTimeZone)
18
+ ::Time.__send__(:include, ByStar::Kernel::InTimeZone)
19
+ ::DateTime.__send__(:include, ByStar::Kernel::InTimeZone)
20
+ ::ActiveSupport::TimeWithZone.__send__(:include, ByStar::Kernel::InTimeZone)
@@ -1,41 +1,41 @@
1
- module ByStar
2
-
3
- module Kernel
4
-
5
- module Time
6
-
7
- # A "Weekend" is defined as the 60-hour period from 15:00 Friday to 03:00 Monday.
8
- # The weekend for a given date will be the the next weekend if the day Mon-Thurs,
9
- # otherwise the current weekend if the day is Fri-Sun.
10
- def beginning_of_weekend
11
- beginning_of_week(:monday).advance(:days => 4) + 15.hours
12
- end
13
-
14
- def end_of_weekend
15
- (beginning_of_weekend + 59.hours).end_of_hour
16
- end
17
-
18
- # A "Fortnight" is defined as a two week period, with the first fortnight of the
19
- # year beginning on 1st January.
20
- def beginning_of_fortnight
21
- (beginning_of_year.to_date + 14 * ((self - beginning_of_year) / 2.weeks).to_i).beginning_of_day
22
- end
23
-
24
- def end_of_fortnight
25
- (beginning_of_fortnight.to_date + 13).end_of_day
26
- end
27
-
28
- # A "Calendar Month" is defined as a month as it appears on a calendar, including days form
29
- # previous/following months which are part of the first/last weeks of the given month.
30
- def beginning_of_calendar_month(*args)
31
- beginning_of_month.beginning_of_week(*args)
32
- end
33
-
34
- def end_of_calendar_month(*args)
35
- end_of_month.end_of_week(*args)
36
- end
37
- end
38
- end
39
- end
40
-
41
- ::Time.__send__(:include, ByStar::Kernel::Time)
1
+ module ByStar
2
+
3
+ module Kernel
4
+
5
+ module Time
6
+
7
+ # A "Weekend" is defined as beginning of Saturday to end of Sunday.
8
+ # The weekend for a given date will be the the next weekend if the day Mon-Thurs,
9
+ # otherwise the current weekend if the day is Fri-Sun.
10
+ def beginning_of_weekend
11
+ beginning_of_week(:monday).advance(days: 5)
12
+ end
13
+
14
+ def end_of_weekend
15
+ (beginning_of_weekend + 47.hours).end_of_hour
16
+ end
17
+
18
+ # A "Fortnight" is defined as a two week period, with the first fortnight of the
19
+ # year beginning on 1st January.
20
+ def beginning_of_fortnight
21
+ (beginning_of_year + 1.fortnight * ((self - beginning_of_year) / 1.fortnight).to_i).beginning_of_day
22
+ end
23
+
24
+ def end_of_fortnight
25
+ (beginning_of_fortnight + 13.days).end_of_day
26
+ end
27
+
28
+ # A "Calendar Month" is defined as a month as it appears on a calendar, including days form
29
+ # previous/following months which are part of the first/last weeks of the given month.
30
+ def beginning_of_calendar_month(*args)
31
+ beginning_of_month.beginning_of_week(*args)
32
+ end
33
+
34
+ def end_of_calendar_month(*args)
35
+ end_of_month.end_of_week(*args)
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ ::Time.__send__(:include, ByStar::Kernel::Time)
@@ -1,118 +1,156 @@
1
- module ByStar
2
-
3
- class ParseError < StandardError; end
4
-
5
- module Normalization
6
-
7
- class << self
8
-
9
- def time(value)
10
- case value
11
- when String then time_string(value)
12
- when DateTime then value.to_time
13
- when Date then value.to_time_in_current_zone
14
- else value
15
- end
16
- end
17
-
18
- def time_string(value)
19
- defined?(Chronic) ? time_string_chronic(value) : time_string_fallback(value)
20
- end
21
-
22
- def time_string_chronic(value)
23
- Chronic.time_class = Time.zone
24
- Chronic.parse(value) || raise(ByStar::ParseError, "Chronic could not parse String #{value.inspect}")
25
- end
26
-
27
- def time_string_fallback(value)
28
- Time.zone.parse(value) || raise(ByStar::ParseError, "Cannot parse String #{value.inspect}")
29
- end
30
-
31
- def week(value, options={})
32
- value = try_string_to_int(value)
33
- case value
34
- when Fixnum then week_fixnum(value, options)
35
- else time(value)
36
- end
37
- end
38
-
39
- def week_fixnum(value, options={})
40
- raise ParseError, 'Week number must be between 0 and 52' unless value.in?(0..52)
41
- time = Time.zone.local(options[:year] || Time.zone.now.year)
42
- time.beginning_of_year + value.to_i.weeks
43
- end
44
-
45
- def fortnight(value, options={})
46
- value = try_string_to_int(value)
47
- case value
48
- when Fixnum then fortnight_fixnum(value, options)
49
- else time(value)
50
- end
51
- end
52
-
53
- def fortnight_fixnum(value, options={})
54
- raise ParseError, 'Fortnight number must be between 0 and 26' unless value.in?(0..26)
55
- time = Time.zone.local(options[:year] || Time.zone.now.year)
56
- time + (value * 2).weeks
57
- end
58
-
59
- def quarter(value, options={})
60
- value = try_string_to_int(value)
61
- case value
62
- when Fixnum then quarter_fixnum(value, options)
63
- else time(value)
64
- end
65
- end
66
-
67
- def quarter_fixnum(value, options={})
68
- raise ParseError, 'Quarter number must be between 1 and 4' unless value.in?(1..4)
69
- time = Time.zone.local(options[:year] || Time.zone.now.year)
70
- time.beginning_of_year + ((value - 1) * 3).months
71
- end
72
-
73
- def month(value, options={})
74
- value = try_string_to_int(value)
75
- case value
76
- when Fixnum, String then month_fixnum(value, options)
77
- else time(value)
78
- end
79
- end
80
-
81
- def month_fixnum(value, options={})
82
- year = options[:year] || Time.zone.now.year
83
- Time.zone.parse "#{year}-#{value}-01"
84
- rescue
85
- raise ParseError, 'Month must be a number between 1 and 12 or a month name'
86
- end
87
-
88
- def year(value, options={})
89
- value = try_string_to_int(value)
90
- case value
91
- when Fixnum then year_fixnum(value)
92
- else time(value)
93
- end
94
- end
95
-
96
- def year_fixnum(value)
97
- Time.zone.local(extrapolate_year(value))
98
- end
99
-
100
- def extrapolate_year(value)
101
- case value.to_i
102
- when 0..69
103
- 2000 + value
104
- when 70..99
105
- 1900 + value
106
- else
107
- value.to_i
108
- end
109
- end
110
-
111
- def try_string_to_int(value)
112
- value.is_a?(String) ? Integer(value) : value
113
- rescue
114
- value
115
- end
116
- end
117
- end
118
- end
1
+ module ByStar
2
+
3
+ class ParseError < StandardError; end
4
+
5
+ module Normalization
6
+
7
+ class << self
8
+
9
+ def date(value)
10
+ value = parse_time(value) if value.is_a?(String)
11
+ value = value.try(:in_time_zone) unless value.is_a?(Date)
12
+ value.try(:to_date)
13
+ end
14
+
15
+ def time(value)
16
+ value = parse_time(value) if value.is_a?(String)
17
+ value.try(:in_time_zone)
18
+ end
19
+
20
+ def week(value, options={})
21
+ value = try_string_to_int(value)
22
+ case value
23
+ when Integer then week_integer(value, options)
24
+ else date(value)
25
+ end
26
+ end
27
+
28
+ def week_integer(value, options={})
29
+ raise ParseError, 'Week number must be between 0 and 52' unless value.in?(0..52)
30
+ time = Time.zone.local(options[:year] || Time.zone.now.year)
31
+ time.beginning_of_year + value.to_i.weeks
32
+ end
33
+
34
+ def cweek(value, options={})
35
+ _value = value
36
+ if _value.is_a?(Integer)
37
+ raise ParseError, 'cweek number must be between 1 and 53' unless value.in?(1..53)
38
+ _value -= 1
39
+ end
40
+ week(_value, options)
41
+ end
42
+
43
+ def fortnight(value, options={})
44
+ value = try_string_to_int(value)
45
+ case value
46
+ when Integer then fortnight_integer(value, options)
47
+ else date(value)
48
+ end
49
+ end
50
+
51
+ def fortnight_integer(value, options={})
52
+ raise ParseError, 'Fortnight number must be between 0 and 26' unless value.in?(0..26)
53
+ time = Time.zone.local(options[:year] || Time.zone.now.year)
54
+ time + (value * 2).weeks
55
+ end
56
+
57
+ def quarter(value, options={})
58
+ value = try_string_to_int(value)
59
+ case value
60
+ when Integer then quarter_integer(value, options)
61
+ else date(value)
62
+ end
63
+ end
64
+
65
+ def quarter_integer(value, options={})
66
+ raise ParseError, 'Quarter number must be between 1 and 4' unless value.in?(1..4)
67
+ time = Time.zone.local(options[:year] || Time.zone.now.year)
68
+ time.beginning_of_year + ((value - 1) * 3).months
69
+ end
70
+
71
+ def month(value, options={})
72
+ value = try_string_to_int(value)
73
+ case value
74
+ when Integer, String then month_integer(value, options)
75
+ else date(value)
76
+ end
77
+ end
78
+
79
+ def month_integer(value, options={})
80
+ year = options[:year] || Time.zone.now.year
81
+ Time.zone.parse "#{year}-#{value}-01"
82
+ rescue
83
+ raise ParseError, 'Month must be a number between 1 and 12 or a month name'
84
+ end
85
+
86
+ def year(value, options={})
87
+ value = try_string_to_int(value)
88
+ case value
89
+ when Integer then year_integer(value)
90
+ else date(value)
91
+ end
92
+ end
93
+
94
+ def year_integer(value)
95
+ Time.zone.local(extrapolate_year(value))
96
+ end
97
+
98
+ def extrapolate_year(value)
99
+ case value.to_i
100
+ when 0..69
101
+ 2000 + value
102
+ when 70..99
103
+ 1900 + value
104
+ else
105
+ value.to_i
106
+ end
107
+ end
108
+
109
+ def try_string_to_int(value)
110
+ value.is_a?(String) ? Integer(value) : value
111
+ rescue
112
+ value
113
+ end
114
+
115
+ def time_in_units(seconds)
116
+ days = seconds / 1.day
117
+ time = Time.at(seconds).utc
118
+ { days: days, hour: time.hour, min: time.min, sec: time.sec }
119
+ end
120
+
121
+ def apply_offset_start(time, offset)
122
+ units = time_in_units(offset)
123
+ time += units.delete(:days).days
124
+ time.change(units)
125
+ end
126
+
127
+ def apply_offset_end(time, offset)
128
+ units = time_in_units(offset)
129
+ time += units.delete(:days).days
130
+ (time + 1.day).change(units) - 1.second
131
+ end
132
+
133
+ def extract_range(args)
134
+ case args[0]
135
+ when Array, Range then [args[0].first, args[0].last]
136
+ else args[0..1]
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ def parse_time(value)
143
+ defined?(Chronic) ? parse_time_chronic(value) : parse_time_fallback(value)
144
+ end
145
+
146
+ def parse_time_chronic(value)
147
+ Chronic.time_class = Time.zone
148
+ Chronic.parse(value) || raise(ByStar::ParseError, "Chronic could not parse String #{value.inspect}")
149
+ end
150
+
151
+ def parse_time_fallback(value)
152
+ Time.zone.parse(value) || raise(ByStar::ParseError, "Cannot parse String #{value.inspect}")
153
+ end
154
+ end
155
+ end
156
+ end
@@ -1,59 +1,75 @@
1
- module ByStar
2
- module ActiveRecord
3
- extend ActiveSupport::Concern
4
-
5
- module ClassMethods
6
- include ::ByStar::Base
7
-
8
- # Returns all records between a given start and finish time.
9
- #
10
- # Currently only supports Time objects.
11
- def between_times_query(start, finish, options={})
12
- start_field = by_star_start_field(options)
13
- end_field = by_star_end_field(options)
14
-
15
- scope = by_star_scope(options)
16
- scope = if options[:strict] || start_field == end_field
17
- scope.where("#{start_field} >= ? AND #{end_field} <= ?", start, finish)
18
- else
19
- scope.where("#{end_field} > ? AND #{start_field} < ?", start, finish)
20
- end
21
- scope = scope.order(options[:order]) if options[:order]
22
- scope
23
- end
24
-
25
- def between(*args)
26
- ActiveSupport::Deprecation.warn 'ByStar `between` method will be removed in v3.0.0. Please use `between_times`'
27
- between_times(*args)
28
- end
29
-
30
- protected
31
-
32
- def by_star_default_field
33
- "#{self.table_name}.created_at"
34
- end
35
-
36
- def before_query(time, options={})
37
- field = by_star_start_field(options)
38
- by_star_scope(options).where("#{field} <= ?", time)
39
- end
40
-
41
- def after_query(time, options={})
42
- field = by_star_start_field(options)
43
- by_star_scope(options).where("#{field} >= ?", time)
44
- end
45
- end
46
-
47
- def previous(options={})
48
- field = self.class.by_star_start_field
49
- value = self.send(field.split(".").last)
50
- self.class.by_star_scope(options).where("#{field} < ?", value).reorder("#{field} DESC").first
51
- end
52
-
53
- def next(options={})
54
- field = self.class.by_star_start_field
55
- value = self.send(field.split(".").last)
56
- self.class.by_star_scope(options).where("#{field} > ?", value).reorder("#{field} ASC").first
57
- end
58
- end
59
- end
1
+ module ByStar
2
+ module ActiveRecord
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ include ::ByStar::Base
7
+
8
+ protected
9
+
10
+ def by_star_default_field
11
+ "#{self.table_name}.created_at"
12
+ end
13
+
14
+ def by_star_point_query(scope, field, start_time, end_time)
15
+ scope.where("#{field} >= ? AND #{field} <= ?", start_time, end_time)
16
+ end
17
+
18
+ def by_star_span_strict_query(scope, start_field, end_field, start_time, end_time)
19
+ scope.where("#{start_field} >= ? AND #{start_field} <= ? AND #{end_field} >= ? AND #{end_field} <= ?", start_time, end_time, start_time, end_time)
20
+ end
21
+
22
+ def by_star_span_loose_query(scope, start_field, end_field, start_time, end_time, options)
23
+ index_scope = by_star_eval_index_scope(start_time, end_time, options)
24
+ scope = scope.where("#{end_field} > ? AND #{start_field} < ?", start_time, end_time)
25
+ scope = scope.where("#{start_field} >= ?", index_scope) if index_scope
26
+ scope
27
+ end
28
+
29
+ def by_star_point_overlap_query(scope, field, time)
30
+ scope.where("#{field} = ?", time)
31
+ end
32
+
33
+ def by_star_span_overlap_query(scope, start_field, end_field, time, options)
34
+ index_scope = by_star_eval_index_scope(time, time, options)
35
+ scope = scope.where("#{end_field} > ? AND #{start_field} <= ?", time, time)
36
+ scope = scope.where("#{start_field} >= ?", index_scope) if index_scope
37
+ scope
38
+ end
39
+
40
+ def by_star_before_query(scope, field, time)
41
+ scope.where("#{field} <= ?", time)
42
+ end
43
+
44
+ def by_star_after_query(scope, field, time)
45
+ scope.where("#{field} >= ?", time)
46
+ end
47
+
48
+ def by_star_order(scope, order)
49
+ scope.order(order)
50
+ end
51
+
52
+ def oldest_query(options={})
53
+ field = by_star_start_field(options)
54
+ reorder("#{field} ASC").first
55
+ end
56
+
57
+ def newest_query(options={})
58
+ field = by_star_start_field(options)
59
+ reorder("#{field} DESC").first
60
+ end
61
+ end
62
+
63
+ def previous(options={})
64
+ field = self.class.by_star_start_field(options)
65
+ value = self.send(field.split(".").last)
66
+ self.class.where("#{field} < ?", value).reorder("#{field} DESC").first
67
+ end
68
+
69
+ def next(options={})
70
+ field = self.class.by_star_start_field(options)
71
+ value = self.send(field.split(".").last)
72
+ self.class.where("#{field} > ?", value).reorder("#{field} ASC").first
73
+ end
74
+ end
75
+ end