nearest 0.0.2 → 1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bd2dbd1bc667a5358a73443710dd7b8a1ecfbec9ce768ac89f824db55ce9b95a
4
+ data.tar.gz: 556bb3486e33520b1517a2ce69ad0d58336af9b5ba8730ba5a9831464917a7b1
5
+ SHA512:
6
+ metadata.gz: baf9badd33d6a39a3439c005755fec697e6aeb1c68d6fd51558297678d8cec80cf3507dc51588141f7ab7736ce23cbb08da6994aa6e40e86cc63564066da66fe
7
+ data.tar.gz: 1b5eef52ca6752b3db438905276fbca3f87304ca1df3feee797dce5329f95a6b33b72eb2a42a90db902bdde269a396eb893a468103c06191bf3271885c18dbfa
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Rounds a time-like object to the nearest interval.
4
+ #
5
+ # The algorithm converts time to epoch seconds, then uses divmod to split into
6
+ # whole intervals (quotient) and leftover seconds (remainder). Multiplying the
7
+ # quotient back by the interval gives the earlier boundary; quotient + 1 gives
8
+ # the later boundary. The remainder decides which one to pick.
9
+ #
10
+ # Example: 1:10pm rounded to 15-minute intervals (900 seconds)
11
+ #
12
+ # epoch_seconds = 47400 (seconds since midnight for 1:10pm)
13
+ # quotient, remainder = 47400.divmod(900) # => [52, 600]
14
+ #
15
+ # quotient * 900 = 46800 => 1:00pm (earlier boundary)
16
+ # (quotient + 1) * 900 = 47700 => 1:15pm (later boundary)
17
+ # remainder = 600 (10 minutes past the earlier boundary)
18
+ #
19
+ # round: :next => quotient + 1 => 1:15pm (always advances, even from a boundary)
20
+ # round: :up => quotient + 1 => 1:15pm
21
+ # round: :nearest => 600 * 2 >= 900, so quotient + 1 => 1:15pm
22
+ # round: :down => quotient => 1:00pm
23
+ # round: :prev => quotient => 1:00pm (always retreats, even from a boundary)
24
+ class Nearest
25
+ @warned_intervals = Set.new
26
+
27
+ class << self
28
+ def warn_once(seconds)
29
+ int = seconds.to_i
30
+ return if (3600 % int).zero?
31
+ return unless @warned_intervals.add?(int)
32
+
33
+ Kernel.warn "nearest: #{int}s does not evenly divide 3600; " \
34
+ 'boundaries may not align with clock minutes. ' \
35
+ 'Use anchor: :hour or anchor: :day for clock-aligned rounding'
36
+ end
37
+
38
+ def reset_warnings!
39
+ @warned_intervals.clear
40
+ end
41
+ end
42
+
43
+ def initialize(time)
44
+ @time = time
45
+ end
46
+
47
+ def nearest(seconds, round: :nearest, anchor: nil)
48
+ anchor_defaulted = anchor.nil?
49
+ anchor ||= :epoch
50
+ validate!(seconds, round, anchor)
51
+ self.class.warn_once(seconds) if anchor_defaulted
52
+ rebuild(rounded_epoch(seconds, round, anchor))
53
+ end
54
+
55
+ private
56
+
57
+ def rounded_epoch(seconds, round, anchor) # rubocop:disable Metrics/CyclomaticComplexity
58
+ base, offset = base_and_offset(anchor)
59
+ quotient, remainder = offset.divmod(seconds)
60
+ rounded = case round
61
+ when :next then quotient + 1 # always advance
62
+ when :up then remainder.zero? ? quotient : quotient + 1 # advance, unless exact
63
+ when :nearest then remainder * 2 >= seconds ? quotient + 1 : quotient # round at midpoint
64
+ when :down then quotient # truncate
65
+ when :prev then remainder.zero? ? quotient - 1 : quotient # always retreat
66
+ end
67
+ base + (rounded * seconds)
68
+ end
69
+
70
+ def base_and_offset(anchor)
71
+ case anchor
72
+ when :epoch
73
+ [0, @time.to_i]
74
+ when :hour
75
+ local_offset = (@time.min * 60) + @time.sec
76
+ [@time.to_i - local_offset, local_offset]
77
+ when :day
78
+ local_offset = (@time.hour * 3600) + (@time.min * 60) + @time.sec
79
+ [@time.to_i - local_offset, local_offset]
80
+ end
81
+ end
82
+
83
+ def rebuild(epoch)
84
+ if @time.respond_to?(:time_zone)
85
+ Time.at(epoch).utc.in_time_zone(@time.time_zone)
86
+ elsif @time.is_a?(DateTime)
87
+ Time.at(epoch, in: @time.to_time.utc_offset).to_datetime
88
+ else
89
+ new_time = Time.at(epoch, in: @time.utc_offset)
90
+ @time.utc? ? new_time.utc : new_time
91
+ end
92
+ end
93
+
94
+ def validate!(seconds, round, anchor)
95
+ raise ArgumentError, 'seconds must be a positive number' unless seconds.is_a?(Numeric) && seconds.positive?
96
+
97
+ unless %i[next up nearest down prev].include?(round)
98
+ raise ArgumentError, "round must be :next, :up, :nearest, :down, or :prev (got #{round.inspect})"
99
+ end
100
+
101
+ return if %i[epoch hour day].include?(anchor)
102
+
103
+ raise ArgumentError, "anchor must be :epoch, :hour, or :day (got #{anchor.inspect})"
104
+ end
105
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'core'
4
+
5
+ class DateTime # rubocop:disable Style/Documentation
6
+ def nearest(seconds, round: :nearest, anchor: nil)
7
+ Nearest.new(self).nearest(seconds, round:, anchor:)
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'core'
4
+
5
+ class Time # rubocop:disable Style/Documentation
6
+ def nearest(seconds, round: :nearest, anchor: nil)
7
+ Nearest.new(self).nearest(seconds, round:, anchor:)
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'core'
4
+
5
+ module ActiveSupport
6
+ class TimeWithZone # rubocop:disable Style/Documentation
7
+ def nearest(seconds, round: :nearest, anchor: nil)
8
+ Nearest.new(self).nearest(seconds, round:, anchor:)
9
+ end
10
+ end
11
+ end
data/lib/nearest.rb CHANGED
@@ -1,8 +1,6 @@
1
- class Time
2
- def nearest(seconds, opts={})
3
- method = opts[:force] ? (opts[:force] == :future ? 'ceil' : 'floor') : 'round'
1
+ # frozen_string_literal: true
4
2
 
5
- new_time = Time.at((self.to_f / seconds).send(method) * seconds)
6
- utc? ? new_time.utc : new_time
7
- end
8
- end
3
+ require_relative 'nearest/core'
4
+ require_relative 'nearest/time'
5
+ require_relative 'nearest/date_time'
6
+ require_relative 'nearest/time_with_zone' if defined?(ActiveSupport::TimeWithZone)
metadata CHANGED
@@ -1,45 +1,48 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nearest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
5
- prerelease:
4
+ version: 1.0.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Aaron Rosenberg
9
- autorequire:
10
8
  bindir: bin
11
9
  cert_chain: []
12
- date: 2012-07-03 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
13
11
  dependencies: []
14
- description: Adds the nearest method to the Time class.
12
+ description: Round Time, DateTime, or ActiveSupport::TimeWithZone to the nearest interval.
13
+ Use the standalone Nearest class or opt-in monkey patches.
15
14
  email: aarongrosenberg@gmail.com
16
15
  executables: []
17
16
  extensions: []
18
17
  extra_rdoc_files: []
19
18
  files:
20
19
  - lib/nearest.rb
21
- homepage: https://github.com/LtCmdDudefellah/nearest
22
- licenses: []
23
- post_install_message:
20
+ - lib/nearest/core.rb
21
+ - lib/nearest/date_time.rb
22
+ - lib/nearest/time.rb
23
+ - lib/nearest/time_with_zone.rb
24
+ homepage: https://github.com/agrberg/nearest
25
+ licenses:
26
+ - MIT
27
+ metadata:
28
+ rubygems_mfa_required: 'true'
29
+ source_code_uri: https://github.com/agrberg/nearest
30
+ bug_tracker_uri: https://github.com/agrberg/nearest/issues
24
31
  rdoc_options: []
25
32
  require_paths:
26
33
  - lib
27
34
  required_ruby_version: !ruby/object:Gem::Requirement
28
- none: false
29
35
  requirements:
30
- - - ! '>='
36
+ - - ">="
31
37
  - !ruby/object:Gem::Version
32
- version: '0'
38
+ version: '3.3'
33
39
  required_rubygems_version: !ruby/object:Gem::Requirement
34
- none: false
35
40
  requirements:
36
- - - ! '>='
41
+ - - ">="
37
42
  - !ruby/object:Gem::Version
38
43
  version: '0'
39
44
  requirements: []
40
- rubyforge_project:
41
- rubygems_version: 1.8.22
42
- signing_key:
43
- specification_version: 3
44
- summary: Find the nearest X minutes to a given time.
45
+ rubygems_version: 3.6.9
46
+ specification_version: 4
47
+ summary: Round times to the nearest interval.
45
48
  test_files: []