activesupport 6.0.0 → 6.1.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activesupport might be problematic. Click here for more details.

Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +381 -349
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -2
  5. data/lib/active_support.rb +13 -1
  6. data/lib/active_support/array_inquirer.rb +4 -2
  7. data/lib/active_support/backtrace_cleaner.rb +3 -4
  8. data/lib/active_support/benchmarkable.rb +1 -1
  9. data/lib/active_support/cache.rb +101 -59
  10. data/lib/active_support/cache/file_store.rb +11 -11
  11. data/lib/active_support/cache/mem_cache_store.rb +34 -33
  12. data/lib/active_support/cache/memory_store.rb +52 -31
  13. data/lib/active_support/cache/null_store.rb +3 -3
  14. data/lib/active_support/cache/redis_cache_store.rb +38 -33
  15. data/lib/active_support/cache/strategy/local_cache.rb +41 -26
  16. data/lib/active_support/callbacks.rb +65 -59
  17. data/lib/active_support/concern.rb +46 -2
  18. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +18 -0
  19. data/lib/active_support/concurrency/share_lock.rb +0 -1
  20. data/lib/active_support/configurable.rb +3 -3
  21. data/lib/active_support/configuration_file.rb +46 -0
  22. data/lib/active_support/core_ext.rb +1 -1
  23. data/lib/active_support/core_ext/array/conversions.rb +5 -5
  24. data/lib/active_support/core_ext/benchmark.rb +2 -2
  25. data/lib/active_support/core_ext/class/attribute.rb +34 -44
  26. data/lib/active_support/core_ext/class/subclasses.rb +17 -38
  27. data/lib/active_support/core_ext/date/conversions.rb +2 -1
  28. data/lib/active_support/core_ext/date_and_time/calculations.rb +13 -0
  29. data/lib/active_support/core_ext/date_and_time/compatibility.rb +15 -0
  30. data/lib/active_support/core_ext/date_and_time/zones.rb +0 -1
  31. data/lib/active_support/core_ext/date_time/conversions.rb +0 -1
  32. data/lib/active_support/core_ext/enumerable.rb +76 -4
  33. data/lib/active_support/core_ext/hash/conversions.rb +3 -3
  34. data/lib/active_support/core_ext/hash/deep_transform_values.rb +1 -1
  35. data/lib/active_support/core_ext/hash/except.rb +1 -1
  36. data/lib/active_support/core_ext/hash/keys.rb +1 -1
  37. data/lib/active_support/core_ext/hash/slice.rb +3 -2
  38. data/lib/active_support/core_ext/load_error.rb +1 -1
  39. data/lib/active_support/core_ext/marshal.rb +2 -0
  40. data/lib/active_support/core_ext/module/attr_internal.rb +2 -2
  41. data/lib/active_support/core_ext/module/attribute_accessors.rb +23 -29
  42. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +8 -4
  43. data/lib/active_support/core_ext/module/concerning.rb +8 -2
  44. data/lib/active_support/core_ext/module/delegation.rb +46 -29
  45. data/lib/active_support/core_ext/module/introspection.rb +2 -25
  46. data/lib/active_support/core_ext/name_error.rb +29 -2
  47. data/lib/active_support/core_ext/numeric/conversions.rb +22 -18
  48. data/lib/active_support/core_ext/object/deep_dup.rb +1 -1
  49. data/lib/active_support/core_ext/object/json.rb +13 -2
  50. data/lib/active_support/core_ext/object/try.rb +4 -2
  51. data/lib/active_support/core_ext/range/compare_range.rb +15 -3
  52. data/lib/active_support/core_ext/range/each.rb +0 -1
  53. data/lib/active_support/core_ext/range/include_time_with_zone.rb +8 -3
  54. data/lib/active_support/core_ext/regexp.rb +8 -1
  55. data/lib/active_support/core_ext/string/access.rb +5 -24
  56. data/lib/active_support/core_ext/string/conversions.rb +1 -0
  57. data/lib/active_support/core_ext/string/inflections.rb +38 -4
  58. data/lib/active_support/core_ext/string/inquiry.rb +1 -0
  59. data/lib/active_support/core_ext/string/multibyte.rb +2 -2
  60. data/lib/active_support/core_ext/string/output_safety.rb +12 -11
  61. data/lib/active_support/core_ext/string/starts_ends_with.rb +2 -2
  62. data/lib/active_support/core_ext/symbol.rb +3 -0
  63. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +14 -0
  64. data/lib/active_support/core_ext/time/calculations.rb +27 -3
  65. data/lib/active_support/core_ext/time/conversions.rb +2 -0
  66. data/lib/active_support/core_ext/uri.rb +5 -1
  67. data/lib/active_support/current_attributes.rb +7 -2
  68. data/lib/active_support/current_attributes/test_helper.rb +13 -0
  69. data/lib/active_support/dependencies.rb +42 -20
  70. data/lib/active_support/dependencies/zeitwerk_integration.rb +9 -2
  71. data/lib/active_support/deprecation.rb +6 -1
  72. data/lib/active_support/deprecation/behaviors.rb +15 -2
  73. data/lib/active_support/deprecation/disallowed.rb +56 -0
  74. data/lib/active_support/deprecation/instance_delegator.rb +0 -1
  75. data/lib/active_support/deprecation/method_wrappers.rb +13 -6
  76. data/lib/active_support/deprecation/proxy_wrappers.rb +6 -2
  77. data/lib/active_support/deprecation/reporting.rb +50 -7
  78. data/lib/active_support/descendants_tracker.rb +6 -3
  79. data/lib/active_support/duration.rb +86 -35
  80. data/lib/active_support/duration/iso8601_parser.rb +0 -1
  81. data/lib/active_support/duration/iso8601_serializer.rb +15 -10
  82. data/lib/active_support/encrypted_file.rb +20 -3
  83. data/lib/active_support/environment_inquirer.rb +20 -0
  84. data/lib/active_support/evented_file_update_checker.rb +69 -134
  85. data/lib/active_support/file_update_checker.rb +0 -1
  86. data/lib/active_support/fork_tracker.rb +62 -0
  87. data/lib/active_support/gem_version.rb +2 -2
  88. data/lib/active_support/hash_with_indifferent_access.rb +43 -24
  89. data/lib/active_support/i18n_railtie.rb +15 -16
  90. data/lib/active_support/inflector/inflections.rb +1 -3
  91. data/lib/active_support/inflector/methods.rb +36 -33
  92. data/lib/active_support/inflector/transliterate.rb +4 -4
  93. data/lib/active_support/json/decoding.rb +4 -5
  94. data/lib/active_support/json/encoding.rb +5 -1
  95. data/lib/active_support/key_generator.rb +1 -1
  96. data/lib/active_support/lazy_load_hooks.rb +0 -1
  97. data/lib/active_support/locale/en.rb +4 -2
  98. data/lib/active_support/locale/en.yml +7 -3
  99. data/lib/active_support/log_subscriber.rb +8 -1
  100. data/lib/active_support/logger.rb +2 -2
  101. data/lib/active_support/logger_silence.rb +2 -26
  102. data/lib/active_support/logger_thread_safe_level.rb +34 -12
  103. data/lib/active_support/message_encryptor.rb +5 -8
  104. data/lib/active_support/message_verifier.rb +7 -7
  105. data/lib/active_support/messages/metadata.rb +11 -2
  106. data/lib/active_support/messages/rotation_configuration.rb +2 -1
  107. data/lib/active_support/messages/rotator.rb +10 -9
  108. data/lib/active_support/multibyte/chars.rb +5 -44
  109. data/lib/active_support/multibyte/unicode.rb +9 -84
  110. data/lib/active_support/notifications.rb +32 -5
  111. data/lib/active_support/notifications/fanout.rb +23 -8
  112. data/lib/active_support/notifications/instrumenter.rb +7 -16
  113. data/lib/active_support/number_helper.rb +33 -14
  114. data/lib/active_support/number_helper/number_converter.rb +5 -6
  115. data/lib/active_support/number_helper/number_to_currency_converter.rb +2 -7
  116. data/lib/active_support/number_helper/number_to_delimited_converter.rb +0 -1
  117. data/lib/active_support/number_helper/number_to_human_converter.rb +1 -2
  118. data/lib/active_support/number_helper/number_to_human_size_converter.rb +1 -2
  119. data/lib/active_support/number_helper/number_to_phone_converter.rb +0 -1
  120. data/lib/active_support/number_helper/number_to_rounded_converter.rb +3 -4
  121. data/lib/active_support/number_helper/rounding_helper.rb +12 -28
  122. data/lib/active_support/option_merger.rb +22 -3
  123. data/lib/active_support/ordered_hash.rb +1 -1
  124. data/lib/active_support/ordered_options.rb +13 -3
  125. data/lib/active_support/parameter_filter.rb +17 -13
  126. data/lib/active_support/per_thread_registry.rb +1 -1
  127. data/lib/active_support/rails.rb +1 -4
  128. data/lib/active_support/railtie.rb +23 -1
  129. data/lib/active_support/rescuable.rb +4 -4
  130. data/lib/active_support/secure_compare_rotator.rb +51 -0
  131. data/lib/active_support/security_utils.rb +19 -12
  132. data/lib/active_support/string_inquirer.rb +4 -3
  133. data/lib/active_support/subscriber.rb +12 -7
  134. data/lib/active_support/tagged_logging.rb +29 -4
  135. data/lib/active_support/testing/assertions.rb +18 -11
  136. data/lib/active_support/testing/parallelization.rb +12 -89
  137. data/lib/active_support/testing/parallelization/server.rb +78 -0
  138. data/lib/active_support/testing/parallelization/worker.rb +100 -0
  139. data/lib/active_support/testing/stream.rb +0 -1
  140. data/lib/active_support/testing/time_helpers.rb +40 -5
  141. data/lib/active_support/time_with_zone.rb +67 -43
  142. data/lib/active_support/values/time_zone.rb +20 -10
  143. data/lib/active_support/xml_mini.rb +0 -1
  144. data/lib/active_support/xml_mini/jdom.rb +0 -1
  145. data/lib/active_support/xml_mini/rexml.rb +8 -1
  146. metadata +39 -38
  147. data/lib/active_support/core_ext/array/prepend_and_append.rb +0 -5
  148. data/lib/active_support/core_ext/hash/compact.rb +0 -5
  149. data/lib/active_support/core_ext/hash/transform_values.rb +0 -5
  150. data/lib/active_support/core_ext/module/reachable.rb +0 -6
  151. data/lib/active_support/core_ext/numeric/inquiry.rb +0 -5
  152. data/lib/active_support/core_ext/range/include_range.rb +0 -9
@@ -17,15 +17,18 @@ module ActiveSupport
17
17
  require "active_support/deprecation/instance_delegator"
18
18
  require "active_support/deprecation/behaviors"
19
19
  require "active_support/deprecation/reporting"
20
+ require "active_support/deprecation/disallowed"
20
21
  require "active_support/deprecation/constant_accessor"
21
22
  require "active_support/deprecation/method_wrappers"
22
23
  require "active_support/deprecation/proxy_wrappers"
23
24
  require "active_support/core_ext/module/deprecation"
25
+ require "concurrent/atomic/thread_local_var"
24
26
 
25
27
  include Singleton
26
28
  include InstanceDelegator
27
29
  include Behavior
28
30
  include Reporting
31
+ include Disallowed
29
32
  include MethodWrapper
30
33
 
31
34
  # The version number in which the deprecated behavior will be removed, by default.
@@ -35,12 +38,14 @@ module ActiveSupport
35
38
  # and the second is a library name.
36
39
  #
37
40
  # ActiveSupport::Deprecation.new('2.0', 'MyLibrary')
38
- def initialize(deprecation_horizon = "6.1", gem_name = "Rails")
41
+ def initialize(deprecation_horizon = "6.2", gem_name = "Rails")
39
42
  self.gem_name = gem_name
40
43
  self.deprecation_horizon = deprecation_horizon
41
44
  # By default, warnings are not silenced and debugging is off.
42
45
  self.silenced = false
43
46
  self.debug = false
47
+ @silenced_thread = Concurrent::ThreadLocalVar.new(false)
48
+ @explicitly_allowed_warnings = Concurrent::ThreadLocalVar.new(nil)
44
49
  end
45
50
  end
46
51
  end
@@ -51,7 +51,7 @@ module ActiveSupport
51
51
  # constant. Available behaviors are:
52
52
  #
53
53
  # [+raise+] Raise <tt>ActiveSupport::DeprecationException</tt>.
54
- # [+stderr+] Log all deprecation warnings to +$stderr+.
54
+ # [+stderr+] Log all deprecation warnings to <tt>$stderr</tt>.
55
55
  # [+log+] Log all deprecation warnings to +Rails.logger+.
56
56
  # [+notify+] Use +ActiveSupport::Notifications+ to notify +deprecation.rails+.
57
57
  # [+silence+] Do nothing.
@@ -67,13 +67,18 @@ module ActiveSupport
67
67
  @behavior ||= [DEFAULT_BEHAVIORS[:stderr]]
68
68
  end
69
69
 
70
+ # Returns the current behavior for disallowed deprecations or if one isn't set, defaults to +:raise+.
71
+ def disallowed_behavior
72
+ @disallowed_behavior ||= [DEFAULT_BEHAVIORS[:raise]]
73
+ end
74
+
70
75
  # Sets the behavior to the specified value. Can be a single value, array,
71
76
  # or an object that responds to +call+.
72
77
  #
73
78
  # Available behaviors:
74
79
  #
75
80
  # [+raise+] Raise <tt>ActiveSupport::DeprecationException</tt>.
76
- # [+stderr+] Log all deprecation warnings to +$stderr+.
81
+ # [+stderr+] Log all deprecation warnings to <tt>$stderr</tt>.
77
82
  # [+log+] Log all deprecation warnings to +Rails.logger+.
78
83
  # [+notify+] Use +ActiveSupport::Notifications+ to notify +deprecation.rails+.
79
84
  # [+silence+] Do nothing.
@@ -92,6 +97,14 @@ module ActiveSupport
92
97
  @behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || arity_coerce(b) }
93
98
  end
94
99
 
100
+ # Sets the behavior for disallowed deprecations (those configured by
101
+ # ActiveSupport::Deprecation.disallowed_warnings=) to the specified
102
+ # value. As with +behavior=+, this can be a single value, array, or an
103
+ # object that responds to +call+.
104
+ def disallowed_behavior=(behavior)
105
+ @disallowed_behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || arity_coerce(b) }
106
+ end
107
+
95
108
  private
96
109
  def arity_coerce(behavior)
97
110
  unless behavior.respond_to?(:call)
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ class Deprecation
5
+ module Disallowed
6
+ # Sets the criteria used to identify deprecation messages which should be
7
+ # disallowed. Can be an array containing strings, symbols, or regular
8
+ # expressions. (Symbols are treated as strings). These are compared against
9
+ # the text of the generated deprecation warning.
10
+ #
11
+ # Additionally the scalar symbol +:all+ may be used to treat all
12
+ # deprecations as disallowed.
13
+ #
14
+ # Deprecations matching a substring or regular expression will be handled
15
+ # using the configured +ActiveSupport::Deprecation.disallowed_behavior+
16
+ # rather than +ActiveSupport::Deprecation.behavior+
17
+ attr_writer :disallowed_warnings
18
+
19
+ # Returns the configured criteria used to identify deprecation messages
20
+ # which should be treated as disallowed.
21
+ def disallowed_warnings
22
+ @disallowed_warnings ||= []
23
+ end
24
+
25
+ private
26
+ def deprecation_disallowed?(message)
27
+ disallowed = ActiveSupport::Deprecation.disallowed_warnings
28
+ return false if explicitly_allowed?(message)
29
+ return true if disallowed == :all
30
+ disallowed.any? do |rule|
31
+ case rule
32
+ when String, Symbol
33
+ message.include?(rule.to_s)
34
+ when Regexp
35
+ rule.match?(message)
36
+ end
37
+ end
38
+ end
39
+
40
+ def explicitly_allowed?(message)
41
+ allowances = @explicitly_allowed_warnings.value
42
+ return false unless allowances
43
+ return true if allowances == :all
44
+ allowances = [allowances] unless allowances.kind_of?(Array)
45
+ allowances.any? do |rule|
46
+ case rule
47
+ when String, Symbol
48
+ message.include?(rule.to_s)
49
+ when Regexp
50
+ rule.match?(message)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/kernel/singleton_class"
4
3
  require "active_support/core_ext/module/delegation"
5
4
 
6
5
  module ActiveSupport
@@ -56,17 +56,24 @@ module ActiveSupport
56
56
  mod = nil
57
57
 
58
58
  method_names.each do |method_name|
59
+ message = options[method_name]
59
60
  if target_module.method_defined?(method_name) || target_module.private_method_defined?(method_name)
60
61
  method = target_module.instance_method(method_name)
61
- target_module.redefine_method(method_name) do |*args, &block|
62
- deprecator.deprecation_warning(method_name, options[method_name])
63
- method.bind(self).call(*args, &block)
62
+ target_module.module_eval do
63
+ redefine_method(method_name) do |*args, &block|
64
+ deprecator.deprecation_warning(method_name, message)
65
+ method.bind(self).call(*args, &block)
66
+ end
67
+ ruby2_keywords(method_name) if respond_to?(:ruby2_keywords, true)
64
68
  end
65
69
  else
66
70
  mod ||= Module.new
67
- mod.define_method(method_name) do |*args, &block|
68
- deprecator.deprecation_warning(method_name, options[method_name])
69
- super(*args, &block)
71
+ mod.module_eval do
72
+ define_method(method_name) do |*args, &block|
73
+ deprecator.deprecation_warning(method_name, message)
74
+ super(*args, &block)
75
+ end
76
+ ruby2_keywords(method_name) if respond_to?(:ruby2_keywords, true)
70
77
  end
71
78
  end
72
79
  end
@@ -121,7 +121,7 @@ module ActiveSupport
121
121
  # (Backtrace information…)
122
122
  # ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
123
123
  class DeprecatedConstantProxy < Module
124
- def self.new(*args, &block)
124
+ def self.new(*args, **options, &block)
125
125
  object = args.first
126
126
 
127
127
  return object unless object
@@ -129,7 +129,7 @@ module ActiveSupport
129
129
  end
130
130
 
131
131
  def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance, message: "#{old_const} is deprecated! Use #{new_const} instead.")
132
- require "active_support/inflector/methods"
132
+ Kernel.require "active_support/inflector/methods"
133
133
 
134
134
  @old_const = old_const
135
135
  @new_const = new_const
@@ -145,6 +145,10 @@ module ActiveSupport
145
145
  target.inspect
146
146
  end
147
147
 
148
+ # Don't give a deprecation warning on methods that IRB may invoke
149
+ # during tab-completion.
150
+ delegate :hash, :instance_methods, :name, :respond_to?, to: :target
151
+
148
152
  # Returns the class of the new constant.
149
153
  #
150
154
  # PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune)
@@ -6,7 +6,7 @@ module ActiveSupport
6
6
  class Deprecation
7
7
  module Reporting
8
8
  # Whether to print a message (silent mode)
9
- attr_accessor :silenced
9
+ attr_writer :silenced
10
10
  # Name of gem where method is deprecated
11
11
  attr_accessor :gem_name
12
12
 
@@ -20,7 +20,11 @@ module ActiveSupport
20
20
 
21
21
  callstack ||= caller_locations(2)
22
22
  deprecation_message(callstack, message).tap do |m|
23
- behavior.each { |b| b.call(m, callstack, deprecation_horizon, gem_name) }
23
+ if deprecation_disallowed?(message)
24
+ disallowed_behavior.each { |b| b.call(m, callstack, deprecation_horizon, gem_name) }
25
+ else
26
+ behavior.each { |b| b.call(m, callstack, deprecation_horizon, gem_name) }
27
+ end
24
28
  end
25
29
  end
26
30
 
@@ -33,11 +37,50 @@ module ActiveSupport
33
37
  # ActiveSupport::Deprecation.warn('something broke!')
34
38
  # end
35
39
  # # => nil
36
- def silence
37
- old_silenced, @silenced = @silenced, true
38
- yield
39
- ensure
40
- @silenced = old_silenced
40
+ def silence(&block)
41
+ @silenced_thread.bind(true, &block)
42
+ end
43
+
44
+ # Allow previously disallowed deprecation warnings within the block.
45
+ # <tt>allowed_warnings</tt> can be an array containing strings, symbols, or regular
46
+ # expressions. (Symbols are treated as strings). These are compared against
47
+ # the text of deprecation warning messages generated within the block.
48
+ # Matching warnings will be exempt from the rules set by
49
+ # +ActiveSupport::Deprecation.disallowed_warnings+
50
+ #
51
+ # The optional <tt>if:</tt> argument accepts a truthy/falsy value or an object that
52
+ # responds to <tt>.call</tt>. If truthy, then matching warnings will be allowed.
53
+ # If falsey then the method yields to the block without allowing the warning.
54
+ #
55
+ # ActiveSupport::Deprecation.disallowed_behavior = :raise
56
+ # ActiveSupport::Deprecation.disallowed_warnings = [
57
+ # "something broke"
58
+ # ]
59
+ #
60
+ # ActiveSupport::Deprecation.warn('something broke!')
61
+ # # => ActiveSupport::DeprecationException
62
+ #
63
+ # ActiveSupport::Deprecation.allow ['something broke'] do
64
+ # ActiveSupport::Deprecation.warn('something broke!')
65
+ # end
66
+ # # => nil
67
+ #
68
+ # ActiveSupport::Deprecation.allow ['something broke'], if: Rails.env.production? do
69
+ # ActiveSupport::Deprecation.warn('something broke!')
70
+ # end
71
+ # # => ActiveSupport::DeprecationException for dev/test, nil for production
72
+ def allow(allowed_warnings = :all, if: true, &block)
73
+ conditional = binding.local_variable_get(:if)
74
+ conditional = conditional.call if conditional.respond_to?(:call)
75
+ if conditional
76
+ @explicitly_allowed_warnings.bind(allowed_warnings, &block)
77
+ else
78
+ yield
79
+ end
80
+ end
81
+
82
+ def silenced
83
+ @silenced || @silenced_thread.value
41
84
  end
42
85
 
43
86
  def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = nil)
@@ -13,6 +13,7 @@ module ActiveSupport
13
13
  descendants = @@direct_descendants[klass]
14
14
  descendants ? descendants.to_a : []
15
15
  end
16
+ alias_method :subclasses, :direct_descendants
16
17
 
17
18
  def descendants(klass)
18
19
  arr = []
@@ -41,7 +42,6 @@ module ActiveSupport
41
42
  end
42
43
 
43
44
  private
44
-
45
45
  def accumulate_descendants(klass, acc)
46
46
  if direct_descendants = @@direct_descendants[klass]
47
47
  direct_descendants.each do |direct_descendant|
@@ -60,6 +60,7 @@ module ActiveSupport
60
60
  def direct_descendants
61
61
  DescendantsTracker.direct_descendants(self)
62
62
  end
63
+ alias_method :subclasses, :direct_descendants
63
64
 
64
65
  def descendants
65
66
  DescendantsTracker.descendants(self)
@@ -78,15 +79,17 @@ module ActiveSupport
78
79
  end
79
80
 
80
81
  def <<(klass)
81
- cleanup!
82
82
  @refs << WeakRef.new(klass)
83
83
  end
84
84
 
85
85
  def each
86
- @refs.each do |ref|
86
+ @refs.reject! do |ref|
87
87
  yield ref.__getobj__
88
+ false
88
89
  rescue WeakRef::RefError
90
+ true
89
91
  end
92
+ self
90
93
  end
91
94
 
92
95
  def refs_size
@@ -39,7 +39,7 @@ module ActiveSupport
39
39
 
40
40
  def +(other)
41
41
  if Duration === other
42
- seconds = value + other.parts[:seconds]
42
+ seconds = value + other.parts.fetch(:seconds, 0)
43
43
  new_parts = other.parts.merge(seconds: seconds)
44
44
  new_value = value + other.value
45
45
 
@@ -51,8 +51,8 @@ module ActiveSupport
51
51
 
52
52
  def -(other)
53
53
  if Duration === other
54
- seconds = value - other.parts[:seconds]
55
- new_parts = other.parts.map { |part, other_value| [part, -other_value] }.to_h
54
+ seconds = value - other.parts.fetch(:seconds, 0)
55
+ new_parts = other.parts.transform_values(&:-@)
56
56
  new_parts = new_parts.merge(seconds: seconds)
57
57
  new_value = value - other.value
58
58
 
@@ -64,7 +64,7 @@ module ActiveSupport
64
64
 
65
65
  def *(other)
66
66
  if Duration === other
67
- new_parts = other.parts.map { |part, other_value| [part, value * other_value] }.to_h
67
+ new_parts = other.parts.transform_values { |other_value| value * other_value }
68
68
  new_value = value * other.value
69
69
 
70
70
  Duration.new(new_value, new_parts)
@@ -147,31 +147,31 @@ module ActiveSupport
147
147
  end
148
148
 
149
149
  def seconds(value) #:nodoc:
150
- new(value, [[:seconds, value]])
150
+ new(value, seconds: value)
151
151
  end
152
152
 
153
153
  def minutes(value) #:nodoc:
154
- new(value * SECONDS_PER_MINUTE, [[:minutes, value]])
154
+ new(value * SECONDS_PER_MINUTE, minutes: value)
155
155
  end
156
156
 
157
157
  def hours(value) #:nodoc:
158
- new(value * SECONDS_PER_HOUR, [[:hours, value]])
158
+ new(value * SECONDS_PER_HOUR, hours: value)
159
159
  end
160
160
 
161
161
  def days(value) #:nodoc:
162
- new(value * SECONDS_PER_DAY, [[:days, value]])
162
+ new(value * SECONDS_PER_DAY, days: value)
163
163
  end
164
164
 
165
165
  def weeks(value) #:nodoc:
166
- new(value * SECONDS_PER_WEEK, [[:weeks, value]])
166
+ new(value * SECONDS_PER_WEEK, weeks: value)
167
167
  end
168
168
 
169
169
  def months(value) #:nodoc:
170
- new(value * SECONDS_PER_MONTH, [[:months, value]])
170
+ new(value * SECONDS_PER_MONTH, months: value)
171
171
  end
172
172
 
173
173
  def years(value) #:nodoc:
174
- new(value * SECONDS_PER_YEAR, [[:years, value]])
174
+ new(value * SECONDS_PER_YEAR, years: value)
175
175
  end
176
176
 
177
177
  # Creates a new Duration from a seconds value that is converted
@@ -181,16 +181,20 @@ module ActiveSupport
181
181
  # ActiveSupport::Duration.build(2716146).parts # => {:months=>1, :days=>1}
182
182
  #
183
183
  def build(value)
184
+ unless value.is_a?(::Numeric)
185
+ raise TypeError, "can't build an #{self.name} from a #{value.class.name}"
186
+ end
187
+
184
188
  parts = {}
185
- remainder = value.to_f
189
+ remainder = value.round(9)
186
190
 
187
191
  PARTS.each do |part|
188
192
  unless part == :seconds
189
193
  part_in_seconds = PARTS_IN_SECONDS[part]
190
194
  parts[part] = remainder.div(part_in_seconds)
191
- remainder = (remainder % part_in_seconds).round(9)
195
+ remainder %= part_in_seconds
192
196
  end
193
- end
197
+ end unless value == 0
194
198
 
195
199
  parts[:seconds] = remainder
196
200
 
@@ -198,7 +202,6 @@ module ActiveSupport
198
202
  end
199
203
 
200
204
  private
201
-
202
205
  def calculate_total_seconds(parts)
203
206
  parts.inject(0) do |total, (part, value)|
204
207
  total + value * PARTS_IN_SECONDS[part]
@@ -207,9 +210,8 @@ module ActiveSupport
207
210
  end
208
211
 
209
212
  def initialize(value, parts) #:nodoc:
210
- @value, @parts = value, parts.to_h
211
- @parts.default = 0
212
- @parts.reject! { |k, v| v.zero? }
213
+ @value, @parts = value, parts
214
+ @parts.reject! { |k, v| v.zero? } unless value == 0
213
215
  end
214
216
 
215
217
  def coerce(other) #:nodoc:
@@ -237,13 +239,12 @@ module ActiveSupport
237
239
  # are treated as seconds.
238
240
  def +(other)
239
241
  if Duration === other
240
- parts = @parts.dup
241
- other.parts.each do |(key, value)|
242
- parts[key] += value
242
+ parts = @parts.merge(other.parts) do |_key, value, other_value|
243
+ value + other_value
243
244
  end
244
245
  Duration.new(value + other.value, parts)
245
246
  else
246
- seconds = @parts[:seconds] + other
247
+ seconds = @parts.fetch(:seconds, 0) + other
247
248
  Duration.new(value + other, @parts.merge(seconds: seconds))
248
249
  end
249
250
  end
@@ -257,9 +258,9 @@ module ActiveSupport
257
258
  # Multiplies this Duration by a Numeric and returns a new Duration.
258
259
  def *(other)
259
260
  if Scalar === other || Duration === other
260
- Duration.new(value * other.value, parts.map { |type, number| [type, number * other.value] })
261
+ Duration.new(value * other.value, parts.transform_values { |number| number * other.value })
261
262
  elsif Numeric === other
262
- Duration.new(value * other, parts.map { |type, number| [type, number * other] })
263
+ Duration.new(value * other, parts.transform_values { |number| number * other })
263
264
  else
264
265
  raise_type_error(other)
265
266
  end
@@ -268,11 +269,11 @@ module ActiveSupport
268
269
  # Divides this Duration by a Numeric and returns a new Duration.
269
270
  def /(other)
270
271
  if Scalar === other
271
- Duration.new(value / other.value, parts.map { |type, number| [type, number / other.value] })
272
+ Duration.new(value / other.value, parts.transform_values { |number| number / other.value })
272
273
  elsif Duration === other
273
274
  value / other.value
274
275
  elsif Numeric === other
275
- Duration.new(value / other, parts.map { |type, number| [type, number / other] })
276
+ Duration.new(value / other, parts.transform_values { |number| number / other })
276
277
  else
277
278
  raise_type_error(other)
278
279
  end
@@ -291,7 +292,11 @@ module ActiveSupport
291
292
  end
292
293
 
293
294
  def -@ #:nodoc:
294
- Duration.new(-value, parts.map { |type, number| [type, -number] })
295
+ Duration.new(-value, parts.transform_values(&:-@))
296
+ end
297
+
298
+ def +@ #:nodoc:
299
+ self
295
300
  end
296
301
 
297
302
  def is_a?(klass) #:nodoc:
@@ -338,12 +343,55 @@ module ActiveSupport
338
343
  # 1.year.to_i # => 31556952
339
344
  #
340
345
  # In such cases, Ruby's core
341
- # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and
342
- # Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision
346
+ # Date[https://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and
347
+ # Time[https://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision
343
348
  # date and time arithmetic.
344
349
  def to_i
345
350
  @value.to_i
346
351
  end
352
+ alias :in_seconds :to_i
353
+
354
+ # Returns the amount of minutes a duration covers as a float
355
+ #
356
+ # 1.day.in_minutes # => 1440.0
357
+ def in_minutes
358
+ in_seconds / SECONDS_PER_MINUTE.to_f
359
+ end
360
+
361
+ # Returns the amount of hours a duration covers as a float
362
+ #
363
+ # 1.day.in_hours # => 24.0
364
+ def in_hours
365
+ in_seconds / SECONDS_PER_HOUR.to_f
366
+ end
367
+
368
+ # Returns the amount of days a duration covers as a float
369
+ #
370
+ # 12.hours.in_days # => 0.5
371
+ def in_days
372
+ in_seconds / SECONDS_PER_DAY.to_f
373
+ end
374
+
375
+ # Returns the amount of weeks a duration covers as a float
376
+ #
377
+ # 2.months.in_weeks # => 8.696
378
+ def in_weeks
379
+ in_seconds / SECONDS_PER_WEEK.to_f
380
+ end
381
+
382
+ # Returns the amount of months a duration covers as a float
383
+ #
384
+ # 9.weeks.in_months # => 2.07
385
+ def in_months
386
+ in_seconds / SECONDS_PER_MONTH.to_f
387
+ end
388
+
389
+ # Returns the amount of years a duration covers as a float
390
+ #
391
+ # 30.days.in_years # => 0.082
392
+ def in_years
393
+ in_seconds / SECONDS_PER_YEAR.to_f
394
+ end
347
395
 
348
396
  # Returns +true+ if +other+ is also a Duration instance, which has the
349
397
  # same parts as this one.
@@ -372,7 +420,7 @@ module ActiveSupport
372
420
  alias :before :ago
373
421
 
374
422
  def inspect #:nodoc:
375
- return "0 seconds" if parts.empty?
423
+ return "#{value} seconds" if parts.empty?
376
424
 
377
425
  parts.
378
426
  sort_by { |unit, _ | PARTS.index(unit) }.
@@ -399,10 +447,15 @@ module ActiveSupport
399
447
  end
400
448
 
401
449
  private
402
-
403
450
  def sum(sign, time = ::Time.current)
404
- parts.inject(time) do |t, (type, number)|
405
- if t.acts_like?(:time) || t.acts_like?(:date)
451
+ unless time.acts_like?(:time) || time.acts_like?(:date)
452
+ raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
453
+ end
454
+
455
+ if parts.empty?
456
+ time.since(sign * value)
457
+ else
458
+ parts.inject(time) do |t, (type, number)|
406
459
  if type == :seconds
407
460
  t.since(sign * number)
408
461
  elsif type == :minutes
@@ -412,8 +465,6 @@ module ActiveSupport
412
465
  else
413
466
  t.advance(type => sign * number)
414
467
  end
415
- else
416
- raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
417
468
  end
418
469
  end
419
470
  end