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 +7 -0
- data/lib/nearest/core.rb +105 -0
- data/lib/nearest/date_time.rb +9 -0
- data/lib/nearest/time.rb +9 -0
- data/lib/nearest/time_with_zone.rb +11 -0
- data/lib/nearest.rb +5 -7
- metadata +21 -18
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
|
data/lib/nearest/core.rb
ADDED
|
@@ -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
|
data/lib/nearest/time.rb
ADDED
|
@@ -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
|
-
|
|
2
|
-
def nearest(seconds, opts={})
|
|
3
|
-
method = opts[:force] ? (opts[:force] == :future ? 'ceil' : 'floor') : 'round'
|
|
1
|
+
# frozen_string_literal: true
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
13
11
|
dependencies: []
|
|
14
|
-
description:
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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: '
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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: []
|