by_star 2.2.0 → 4.0.0

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