From 0280d519c2170a9cd17ed403340fc5aa5ad00635 Mon Sep 17 00:00:00 2001 From: "Michael G. Schwern" Date: Sat, 16 Dec 2023 20:51:02 -0800 Subject: [PATCH 1/2] Have the user pass in an ActiveSupport::Duration. Simplifies everything greatly. We need it anyway. We can do a version without ActiveSupport if folks want it. Also * Remove all the core overrides, it's rude for a gem to do that. They were for the app TimeIterator came from. * Remove `quarter`, a user can do a 3 month duration. --- .rubocop.yml | 3 + lib/time_iterator.rb | 29 ++---- lib/time_iterator/core_ext/numeric.rb | 7 -- lib/time_iterator/core_ext/time.rb | 48 --------- spec/time_iterator/core_ext/numeric_spec.rb | 13 --- spec/time_iterator/core_ext/time_spec.rb | 102 -------------------- spec/time_iterator_spec.rb | 37 ++++--- 7 files changed, 27 insertions(+), 212 deletions(-) delete mode 100644 lib/time_iterator/core_ext/numeric.rb delete mode 100644 lib/time_iterator/core_ext/time.rb delete mode 100644 spec/time_iterator/core_ext/numeric_spec.rb delete mode 100644 spec/time_iterator/core_ext/time_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 2f00bf1..c233e44 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -44,6 +44,9 @@ Style/AccessModifierDeclarations: Style/AsciiComments: Enabled: false +Style/BlockDelimiters: + EnforcedStyle: "braces_for_chaining" + Style/FrozenStringLiteralComment: Enabled: false diff --git a/lib/time_iterator.rb b/lib/time_iterator.rb index 5cc3d8d..7d9fafb 100644 --- a/lib/time_iterator.rb +++ b/lib/time_iterator.rb @@ -1,32 +1,17 @@ require "active_support" -require "active_support/core_ext/integer/time" -require_relative "time_iterator/core_ext/numeric" -require_relative "time_iterator/core_ext/time" +require "active_support/duration" # Time iteration. module TimeIterator - INFINITY = 1.0 / 0.0 - PERIODS = { - second: :seconds, - minute: :minutes, - hour: :hours, - day: :days, - week: :weeks, - month: :months, - quarter: :quarters, - year: :years - }.freeze - ITERATE_BY = (PERIODS.keys + PERIODS.values).freeze - class << self - def iterate(start, by:, every: 1) - by = by.to_sym - - raise ArgumentError, "Unknown period to iterate by: #{by}" unless ITERATE_BY.include?(by) + def iterate(start, by:) + raise ArgumentError, "`by` must be an ActiveSupport::Duration" unless by.is_a?(ActiveSupport::Duration) Enumerator.new do |block| - (0..INFINITY).each do |num| - block << (start + (num.send(by) * every)) + time = start + loop do |_num| + block << time + time += by end end end diff --git a/lib/time_iterator/core_ext/numeric.rb b/lib/time_iterator/core_ext/numeric.rb deleted file mode 100644 index 8739872..0000000 --- a/lib/time_iterator/core_ext/numeric.rb +++ /dev/null @@ -1,7 +0,0 @@ -# Inject 3.quarters -class Numeric - def quarter - (3 * self).months - end - alias quarters quarter -end diff --git a/lib/time_iterator/core_ext/time.rb b/lib/time_iterator/core_ext/time.rb deleted file mode 100644 index 0d61719..0000000 --- a/lib/time_iterator/core_ext/time.rb +++ /dev/null @@ -1,48 +0,0 @@ -# Inject convenience methods into Time. -class Time - private def valid_period?(period) - raise ArgumentError, "Unknown time period: #{period}" unless TimeIterator::PERIODS.include?(period) - end - - private def method_for_period(method, period) - period = period.to_sym - valid_period?(period) - send("#{method}_#{period}") - end - - def beginning_of(period) - method_for_period(:beginning_of, period) - end - - def end_of(period) - method_for_period(:end_of, period) - end - - def days_in_month - ::Time.days_in_month( month, year ) - end - - def days_in_year - ::Time.days_in_year( year ) - end - - # Creates a Range from the Time to the end of today - # @return [Range] - def span_to_end_of_today - beginning_of_next_day = ::Time.current.tomorrow.beginning_of_day - (self...beginning_of_next_day) - end - - # @return [String] the abbreviated day of the week - def dow - strftime("%a") - end - - def iso_date - strftime("%Y-%m-%d") - end - - def human_date - strftime("%a %b %-d %Y") - end -end diff --git a/spec/time_iterator/core_ext/numeric_spec.rb b/spec/time_iterator/core_ext/numeric_spec.rb deleted file mode 100644 index 78d7ec7..0000000 --- a/spec/time_iterator/core_ext/numeric_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -RSpec.describe Numeric do - let(:time) { Time.now } - - describe '#quarter(s)' do - [:quarter, :quarters].each do |method| - it 'adds 3 months per quarter' do - expect( time + 1.send(method) ).to eq time + 3.months - expect( time + 3.send(method) ).to eq time + 9.months - expect( time + 4.send(method) ).to eq time + 1.year - end - end - end -end diff --git a/spec/time_iterator/core_ext/time_spec.rb b/spec/time_iterator/core_ext/time_spec.rb deleted file mode 100644 index 5914234..0000000 --- a/spec/time_iterator/core_ext/time_spec.rb +++ /dev/null @@ -1,102 +0,0 @@ -RSpec.describe Time do - let(:time) { described_class.now } - - describe '#beginning_of' do - periods2methods = { - minute: :beginning_of_minute, - hour: :beginning_of_hour, - day: :beginning_of_day, - week: :beginning_of_week, - month: :beginning_of_month, - quarter: :beginning_of_quarter, - year: :beginning_of_year - }.freeze - - periods2methods.each do |period, method| - it "handles #{period}" do - expect(time.beginning_of(period)).to eq time.send(method) - end - end - - it 'raises on invalid period' do - expect { time.beginning_of(:time) }.to raise_error(ArgumentError) - end - end - - describe '#end_of' do - periods2methods = { - minute: :end_of_minute, - hour: :end_of_hour, - day: :end_of_day, - week: :end_of_week, - month: :end_of_month, - quarter: :end_of_quarter, - year: :end_of_year - }.freeze - - periods2methods.each do |period, method| - it "handles #{period}" do - expect(time.end_of(period)).to eq time.send(method) - end - end - - it 'raises on invalid period' do - expect { time.end_of(:universe) }.to raise_error(ArgumentError) - end - end - - describe '#days_in_month/year' do - # Make sure it's accounting for a leap year - let(:time) do - described_class.now.change(year: 2020, month: 2, day: 5) - end - - it 'gives the days in the current month' do - expect( time.days_in_month ).to eq 29 - expect( time.days_in_year ).to eq 366 - end - end - - describe '#span_to_end_of_today', travel_to: described_class.now do - let(:span_begin) { 7.days.ago } - let(:span_end) { described_class.now.tomorrow.beginning_of_day } - let(:span) { span_begin.span_to_end_of_today } - - it 'creates an excluding span to the end of today' do - expect( span.begin ).to eq span_begin - expect( span.end ).to eq span_end - expect( span ).to be_exclude_end - end - - it 'covers from the start and excluding the end' do - # See https://github.com/rubocop-hq/rubocop-rspec/issues/926 for why it isn't using to include. - # See https://github.com/rubocop/rubocop-rspec/issues/466 for why its disabled (be_cover(start_span)) - # rubocop:disable RSpec/PredicateMatcher - expect( span.cover?(span_begin) ).to be_truthy - expect( span.cover?(span_end) ).to be_falsey - expect( span.cover?(span_end - 1.second) ).to be_truthy - # rubocop:enable RSpec/PredicateMatcher - end - end - - describe '#dow' do - it 'returns the abbreviated day of the week' do - wednesday = described_class.local(2019, 9, 18) - expect( wednesday.dow ).to eq 'Wed' - end - end - - describe '#iso_date' do - it 'returns an ISO date' do - time = described_class.local(2019, 9, 8) - expect( time.iso_date ).to eq '2019-09-08' - end - end - - describe '#human_date' do - it 'formats the date for humans' do - sunday = described_class.local(2019, 9, 8) - expect( sunday.human_date ).to eq 'Sun Sep 8 2019' - end - end -end diff --git a/spec/time_iterator_spec.rb b/spec/time_iterator_spec.rb index 3b7d808..2c31cab 100644 --- a/spec/time_iterator_spec.rb +++ b/spec/time_iterator_spec.rb @@ -1,37 +1,34 @@ RSpec.describe TimeIterator do - let(:time) { Time.now } + let(:time) { Time.gm(2000, 1, 1, 0, 0, 0) } describe '.iterate' do it 'uses an Enumerator' do - expect( described_class.iterate(time, by: :day) ).to be_a(Enumerator) + expect( + described_class.iterate(time, by: ActiveSupport::Duration.days(1)) + ).to be_a(Enumerator) end - it 'raises on an invalid period' do - expect do + it 'raises on an invalid `by`' do + expect { described_class.iterate(time, by: :colors) - end.to raise_error(ArgumentError) - end - - [ - :second, :minute, :hour, :day, :week, :month, :quarter, :year, - :seconds, :minutes, :hours, :days, :weeks, :months, :quarter, :years - ].each do |period| - it "iterates by #{period}" do - expect( - described_class.iterate(time, by: period).take(3) - ).to eq [time, time + 1.send(period), time + 2.send(period)] - end + }.to raise_error(ArgumentError) end - it 'iterates every X periods' do + it "iterates" do expect( - described_class.iterate(time, by: :day, every: 3).take(3) - ).to eq [time, time + 3.days, time + 6.days] + described_class.iterate( + Time.gm(2000, 1, 1), by: ActiveSupport::Duration.days(3) + ).take(3) + ).to eq [ + Time.gm(2000, 1, 1), + Time.gm(2000, 1, 4), + Time.gm(2000, 1, 7) + ] end it 'does not alter the original time' do orig = time.clone - described_class.iterate(time, by: :day).take(5) + described_class.iterate(time, by: ActiveSupport::Duration.weeks(2)).take(5) expect( time ).to eq orig end end From ec252fff56abec7d26df3e698a9e9804c1329552 Mon Sep 17 00:00:00 2001 From: "Michael G. Schwern" Date: Sat, 16 Dec 2023 21:07:32 -0800 Subject: [PATCH 2/2] Add an `until` argument. Having a special Enumerator for infinite iteration is potentially a smidge faster. --- lib/time_iterator.rb | 18 ++++++++++++++++-- spec/time_iterator_spec.rb | 30 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/lib/time_iterator.rb b/lib/time_iterator.rb index 7d9fafb..a5e5330 100644 --- a/lib/time_iterator.rb +++ b/lib/time_iterator.rb @@ -4,12 +4,26 @@ # Time iteration. module TimeIterator class << self - def iterate(start, by:) + def iterate(start, by:, to: nil) raise ArgumentError, "`by` must be an ActiveSupport::Duration" unless by.is_a?(ActiveSupport::Duration) + return to ? iterate_to(start, by: by, to: to) : iterate_endless(start, by: by) + end + + private def iterate_endless(start, by:) + Enumerator.new do |block| + time = start + loop do + block << time + time += by + end + end + end + + private def iterate_to(start, by:, to:) Enumerator.new do |block| time = start - loop do |_num| + while time <= to block << time time += by end diff --git a/spec/time_iterator_spec.rb b/spec/time_iterator_spec.rb index 2c31cab..e517da9 100644 --- a/spec/time_iterator_spec.rb +++ b/spec/time_iterator_spec.rb @@ -26,6 +26,36 @@ ] end + it "stops before `to`" do + expect( + described_class.iterate( + Time.gm(2000, 1, 1), + by: ActiveSupport::Duration.days(3), + to: Time.gm(2000, 1, 12) + ).to_a + ).to eq [ + Time.gm(2000, 1, 1), + Time.gm(2000, 1, 4), + Time.gm(2000, 1, 7), + Time.gm(2000, 1, 10) + ] + end + + it "includes `to`" do + expect( + described_class.iterate( + Time.gm(2000, 1, 1), + by: ActiveSupport::Duration.days(3), + to: Time.gm(2000, 1, 10) + ).to_a + ).to eq [ + Time.gm(2000, 1, 1), + Time.gm(2000, 1, 4), + Time.gm(2000, 1, 7), + Time.gm(2000, 1, 10) + ] + end + it 'does not alter the original time' do orig = time.clone described_class.iterate(time, by: ActiveSupport::Duration.weeks(2)).take(5)