require "abstract_unit" require "active_support/inflector" require "active_support/time" require "active_support/json" require "time_zone_test_helpers" class DurationTest < ActiveSupport::TestCase include TimeZoneTestHelpers def test_is_a d = 1.day assert d.is_a?(ActiveSupport::Duration) assert_kind_of ActiveSupport::Duration, d assert_kind_of Numeric, d assert_kind_of Integer, d assert !d.is_a?(Hash) k = Class.new class << k; undef_method :== end assert !d.is_a?(k) end def test_instance_of assert 1.minute.instance_of?(1.class) assert 2.days.instance_of?(ActiveSupport::Duration) assert !3.second.instance_of?(Numeric) end def test_threequals assert ActiveSupport::Duration === 1.day assert !(ActiveSupport::Duration === 1.day.to_i) assert !(ActiveSupport::Duration === "foo") end def test_equals assert 1.day == 1.day assert 1.day == 1.day.to_i assert 1.day.to_i == 1.day assert !(1.day == "foo") end def test_to_s assert_equal "1", 1.second.to_s end def test_eql rubinius_skip "Rubinius' #eql? definition relies on #instance_of? " \ "which behaves oddly for the sake of backward-compatibility." assert 1.minute.eql?(1.minute) assert 1.minute.eql?(60.seconds) assert 2.days.eql?(48.hours) assert !1.second.eql?(1) assert !1.eql?(1.second) assert 1.minute.eql?(180.seconds - 2.minutes) assert !1.minute.eql?(60) assert !1.minute.eql?("foo") end def test_inspect assert_equal "0 seconds", 0.seconds.inspect assert_equal "1 month", 1.month.inspect assert_equal "1 month and 1 day", (1.month + 1.day).inspect assert_equal "6 months and -2 days", (6.months - 2.days).inspect assert_equal "10 seconds", 10.seconds.inspect assert_equal "10 years, 2 months, and 1 day", (10.years + 2.months + 1.day).inspect assert_equal "10 years, 2 months, and 1 day", (10.years + 1.month + 1.day + 1.month).inspect assert_equal "10 years, 2 months, and 1 day", (1.day + 10.years + 2.months).inspect assert_equal "7 days", 7.days.inspect assert_equal "1 week", 1.week.inspect assert_equal "2 weeks", 1.fortnight.inspect end def test_inspect_locale current_locale = I18n.default_locale I18n.default_locale = :de I18n.backend.store_translations(:de, support: { array: { last_word_connector: " und " } }) assert_equal "10 years, 1 month und 1 day", (10.years + 1.month + 1.day).inspect ensure I18n.default_locale = current_locale end def test_minus_with_duration_does_not_break_subtraction_of_date_from_date assert_nothing_raised { Date.today - Date.today } end def test_plus_with_time assert_equal 1 + 1.second, 1.second + 1, "Duration + Numeric should == Numeric + Duration" end def test_time_plus_duration_returns_same_time_datatype twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Moscow"] , Time.utc(2016, 4, 28, 00, 45)) now = Time.now.utc %w( second minute hour day week month year ).each do |unit| assert_equal((now + 1.send(unit)).class, Time, "Time + 1.#{unit} must be Time") assert_equal((twz + 1.send(unit)).class, ActiveSupport::TimeWithZone, "TimeWithZone + 1.#{unit} must be TimeWithZone") end end def test_argument_error e = assert_raise ArgumentError do 1.second.ago("") end assert_equal 'expected a time or date, got ""', e.message, "ensure ArgumentError is not being raised by dependencies.rb" end def test_fractional_weeks assert_equal((86400 * 7) * 1.5, 1.5.weeks) assert_equal((86400 * 7) * 1.7, 1.7.weeks) end def test_fractional_days assert_equal 86400 * 1.5, 1.5.days assert_equal 86400 * 1.7, 1.7.days end def test_since_and_ago t = Time.local(2000) assert_equal t + 1, 1.second.since(t) assert_equal t - 1, 1.second.ago(t) end def test_since_and_ago_without_argument now = Time.now assert 1.second.since >= now + 1 now = Time.now assert 1.second.ago >= now - 1 end def test_since_and_ago_with_fractional_days t = Time.local(2000) # since assert_equal 36.hours.since(t), 1.5.days.since(t) assert_in_delta((24 * 1.7).hours.since(t), 1.7.days.since(t), 1) # ago assert_equal 36.hours.ago(t), 1.5.days.ago(t) assert_in_delta((24 * 1.7).hours.ago(t), 1.7.days.ago(t), 1) end def test_since_and_ago_with_fractional_weeks t = Time.local(2000) # since assert_equal((7 * 36).hours.since(t), 1.5.weeks.since(t)) assert_in_delta((7 * 24 * 1.7).hours.since(t), 1.7.weeks.since(t), 1) # ago assert_equal((7 * 36).hours.ago(t), 1.5.weeks.ago(t)) assert_in_delta((7 * 24 * 1.7).hours.ago(t), 1.7.weeks.ago(t), 1) end def test_since_and_ago_anchored_to_time_now_when_time_zone_is_not_set Time.zone = nil with_env_tz "US/Eastern" do Time.stub(:now, Time.local(2000)) do # since assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.since assert_equal Time.local(2000, 1, 1, 0, 0, 5), 5.seconds.since # ago assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago assert_equal Time.local(1999, 12, 31, 23, 59, 55), 5.seconds.ago end end end def test_since_and_ago_anchored_to_time_zone_now_when_time_zone_is_set Time.zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] with_env_tz "US/Eastern" do Time.stub(:now, Time.local(2000)) do # since assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.since assert_equal Time.utc(2000, 1, 1, 0, 0, 5), 5.seconds.since.time assert_equal "Eastern Time (US & Canada)", 5.seconds.since.time_zone.name # ago assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago assert_equal Time.utc(1999, 12, 31, 23, 59, 55), 5.seconds.ago.time assert_equal "Eastern Time (US & Canada)", 5.seconds.ago.time_zone.name end end ensure Time.zone = nil end def test_adding_hours_across_dst_boundary with_env_tz "CET" do assert_equal Time.local(2009, 3, 29, 0, 0, 0) + 24.hours, Time.local(2009, 3, 30, 1, 0, 0) end end def test_adding_day_across_dst_boundary with_env_tz "CET" do assert_equal Time.local(2009, 3, 29, 0, 0, 0) + 1.day, Time.local(2009, 3, 30, 0, 0, 0) end end def test_delegation_with_block_works counter = 0 assert_nothing_raised do 1.minute.times { counter += 1 } end assert_equal 60, counter end def test_as_json assert_equal 172800, 2.days.as_json end def test_to_json assert_equal "172800", 2.days.to_json end def test_case_when cased = \ case 1.day when 1.day "ok" end assert_equal "ok", cased end def test_respond_to assert_respond_to 1.day, :since assert_respond_to 1.day, :zero? end def test_hash assert_equal 1.minute.hash, 60.seconds.hash end def test_comparable assert_equal(-1, (0.seconds <=> 1.second)) assert_equal(-1, (1.second <=> 1.minute)) assert_equal(-1, (1 <=> 1.minute)) assert_equal(0, (0.seconds <=> 0.seconds)) assert_equal(0, (0.seconds <=> 0.minutes)) assert_equal(0, (1.second <=> 1.second)) assert_equal(1, (1.second <=> 0.second)) assert_equal(1, (1.minute <=> 1.second)) assert_equal(1, (61 <=> 1.minute)) end # ISO8601 string examples are taken from ISO8601 gem at https://github.com/arnau/ISO8601/blob/b93d466840/spec/iso8601/duration_spec.rb # published under the conditions of MIT license at https://github.com/arnau/ISO8601/blob/b93d466840/LICENSE # # Copyright (c) 2012-2014 Arnau Siches # # MIT License # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. def test_iso8601_parsing_wrong_patterns_with_raise invalid_patterns = ["", "P", "PT", "P1YT", "T", "PW", "P1Y1W", "~P1Y", ".P1Y", "P1.5Y0.5M", "P1.5Y1M", "P1.5MT10.5S"] invalid_patterns.each do |pattern| assert_raise ActiveSupport::Duration::ISO8601Parser::ParsingError, pattern.inspect do ActiveSupport::Duration.parse(pattern) end end end def test_iso8601_output expectations = [ ["P1Y", 1.year ], ["P1W", 1.week ], ["P1Y1M", 1.year + 1.month ], ["P1Y1M1D", 1.year + 1.month + 1.day ], ["-P1Y1D", -1.year - 1.day ], ["P1Y-1DT-1S", 1.year - 1.day - 1.second ], # Parts with different signs are exists in PostgreSQL interval datatype. ["PT1S", 1.second ], ["PT1.4S", (1.4).seconds ], ["P1Y1M1DT1H", 1.year + 1.month + 1.day + 1.hour], ["PT0S", 0.minutes ], ] expectations.each do |expected_output, duration| assert_equal expected_output, duration.iso8601, expected_output.inspect end end def test_iso8601_output_precision expectations = [ [nil, "P1Y1MT8.55S", 1.year + 1.month + (8.55).seconds ], [0, "P1Y1MT9S", 1.year + 1.month + (8.55).seconds ], [1, "P1Y1MT8.6S", 1.year + 1.month + (8.55).seconds ], [2, "P1Y1MT8.55S", 1.year + 1.month + (8.55).seconds ], [3, "P1Y1MT8.550S", 1.year + 1.month + (8.55).seconds ], [nil, "PT1S", 1.second ], [2, "PT1.00S", 1.second ], [nil, "PT1.4S", (1.4).seconds ], [0, "PT1S", (1.4).seconds ], [1, "PT1.4S", (1.4).seconds ], [5, "PT1.40000S", (1.4).seconds ], ] expectations.each do |precision, expected_output, duration| assert_equal expected_output, duration.iso8601(precision: precision), expected_output.inspect end end def test_iso8601_output_and_reparsing patterns = %w[ P1Y P0.5Y P0,5Y P1Y1M P1Y0.5M P1Y0,5M P1Y1M1D P1Y1M0.5D P1Y1M0,5D P1Y1M1DT1H P1Y1M1DT0.5H P1Y1M1DT0,5H P1W +P1Y -P1Y P1Y1M1DT1H1M P1Y1M1DT1H0.5M P1Y1M1DT1H0,5M P1Y1M1DT1H1M1S P1Y1M1DT1H1M1.0S P1Y1M1DT1H1M1,0S P-1Y-2M3DT-4H-5M-6S ] # That could be weird, but if we parse P1Y1M0.5D and output it to ISO 8601, we'll get P1Y1MT12.0H. # So we check that initially parsed and reparsed duration added to time will result in the same time. time = Time.current patterns.each do |pattern| duration = ActiveSupport::Duration.parse(pattern) assert_equal time + duration, time + ActiveSupport::Duration.parse(duration.iso8601), pattern.inspect end end def test_iso8601_parsing_across_spring_dst_boundary with_env_tz eastern_time_zone do with_tz_default "Eastern Time (US & Canada)" do travel_to Time.utc(2016, 3, 11) do assert_equal 604800, ActiveSupport::Duration.parse("P7D").to_i assert_equal 604800, ActiveSupport::Duration.parse("P1W").to_i end end end end def test_iso8601_parsing_across_autumn_dst_boundary with_env_tz eastern_time_zone do with_tz_default "Eastern Time (US & Canada)" do travel_to Time.utc(2016, 11, 4) do assert_equal 604800, ActiveSupport::Duration.parse("P7D").to_i assert_equal 604800, ActiveSupport::Duration.parse("P1W").to_i assert_equal ActiveSupport::Duration.parse(3.years.iso8601).to_i, 3.years.to_i end end end end def test_adding_durations_do_not_hold_prior_states time = Time.parse("Nov 29, 2016") # If the implementation adds and subtracts 3 months, the # resulting date would have been in February so the day will # change to the 29th. d1 = 3.months - 3.months d2 = 2.months - 2.months assert_equal time + d1, time + d2 end private def eastern_time_zone if Gem.win_platform? "EST5EDT" else "America/New_York" end end end