activesupport 4.2.11.3 → 5.0.7.2

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.

Potentially problematic release.


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

Files changed (182) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +678 -348
  3. data/MIT-LICENSE +2 -2
  4. data/README.rdoc +2 -3
  5. data/lib/active_support/array_inquirer.rb +44 -0
  6. data/lib/active_support/backtrace_cleaner.rb +1 -1
  7. data/lib/active_support/benchmarkable.rb +1 -1
  8. data/lib/active_support/cache/file_store.rb +36 -22
  9. data/lib/active_support/cache/mem_cache_store.rb +63 -54
  10. data/lib/active_support/cache/memory_store.rb +16 -21
  11. data/lib/active_support/cache/null_store.rb +1 -4
  12. data/lib/active_support/cache/strategy/local_cache.rb +31 -20
  13. data/lib/active_support/cache/strategy/local_cache_middleware.rb +4 -4
  14. data/lib/active_support/cache.rb +71 -87
  15. data/lib/active_support/callbacks.rb +109 -113
  16. data/lib/active_support/concern.rb +1 -1
  17. data/lib/active_support/concurrency/latch.rb +11 -12
  18. data/lib/active_support/concurrency/share_lock.rb +226 -0
  19. data/lib/active_support/configurable.rb +1 -0
  20. data/lib/active_support/core_ext/array/access.rb +27 -1
  21. data/lib/active_support/core_ext/array/conversions.rb +6 -4
  22. data/lib/active_support/core_ext/array/grouping.rb +9 -18
  23. data/lib/active_support/core_ext/array/inquiry.rb +17 -0
  24. data/lib/active_support/core_ext/array/wrap.rb +5 -4
  25. data/lib/active_support/core_ext/array.rb +1 -0
  26. data/lib/active_support/core_ext/big_decimal/conversions.rb +8 -10
  27. data/lib/active_support/core_ext/class/attribute.rb +10 -9
  28. data/lib/active_support/core_ext/class/subclasses.rb +3 -2
  29. data/lib/active_support/core_ext/class.rb +0 -1
  30. data/lib/active_support/core_ext/date/blank.rb +12 -0
  31. data/lib/active_support/core_ext/date/calculations.rb +1 -1
  32. data/lib/active_support/core_ext/date/conversions.rb +7 -6
  33. data/lib/active_support/core_ext/date.rb +1 -1
  34. data/lib/active_support/core_ext/date_and_time/calculations.rb +100 -27
  35. data/lib/active_support/core_ext/date_and_time/compatibility.rb +0 -1
  36. data/lib/active_support/core_ext/date_and_time/zones.rb +3 -4
  37. data/lib/active_support/core_ext/date_time/blank.rb +12 -0
  38. data/lib/active_support/core_ext/date_time/calculations.rb +14 -8
  39. data/lib/active_support/core_ext/date_time/conversions.rb +2 -0
  40. data/lib/active_support/core_ext/date_time.rb +1 -1
  41. data/lib/active_support/core_ext/enumerable.rb +75 -25
  42. data/lib/active_support/core_ext/file/atomic.rb +30 -25
  43. data/lib/active_support/core_ext/hash/conversions.rb +22 -2
  44. data/lib/active_support/core_ext/hash/deep_merge.rb +1 -1
  45. data/lib/active_support/core_ext/hash/except.rb +9 -8
  46. data/lib/active_support/core_ext/hash/indifferent_access.rb +1 -1
  47. data/lib/active_support/core_ext/hash/keys.rb +25 -21
  48. data/lib/active_support/core_ext/hash/slice.rb +1 -1
  49. data/lib/active_support/core_ext/hash/transform_values.rb +11 -5
  50. data/lib/active_support/core_ext/integer/time.rb +2 -2
  51. data/lib/active_support/core_ext/kernel/concern.rb +2 -0
  52. data/lib/active_support/core_ext/kernel/debugger.rb +3 -10
  53. data/lib/active_support/core_ext/kernel/reporting.rb +2 -84
  54. data/lib/active_support/core_ext/kernel.rb +0 -1
  55. data/lib/active_support/core_ext/load_error.rb +5 -2
  56. data/lib/active_support/core_ext/marshal.rb +7 -9
  57. data/lib/active_support/core_ext/module/aliasing.rb +6 -1
  58. data/lib/active_support/core_ext/module/anonymous.rb +10 -1
  59. data/lib/active_support/core_ext/module/attr_internal.rb +2 -5
  60. data/lib/active_support/core_ext/module/attribute_accessors.rb +15 -15
  61. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +141 -0
  62. data/lib/active_support/core_ext/module/concerning.rb +4 -4
  63. data/lib/active_support/core_ext/module/delegation.rb +11 -20
  64. data/lib/active_support/core_ext/module/deprecation.rb +2 -2
  65. data/lib/active_support/core_ext/module/introspection.rb +8 -2
  66. data/lib/active_support/core_ext/module/method_transplanting.rb +3 -13
  67. data/lib/active_support/core_ext/module/qualified_const.rb +30 -12
  68. data/lib/active_support/core_ext/module/remove_method.rb +23 -0
  69. data/lib/active_support/core_ext/module.rb +1 -0
  70. data/lib/active_support/core_ext/name_error.rb +15 -2
  71. data/lib/active_support/core_ext/numeric/bytes.rb +20 -0
  72. data/lib/active_support/core_ext/numeric/conversions.rb +78 -77
  73. data/lib/active_support/core_ext/numeric/inquiry.rb +26 -0
  74. data/lib/active_support/core_ext/numeric/time.rb +26 -6
  75. data/lib/active_support/core_ext/numeric.rb +1 -0
  76. data/lib/active_support/core_ext/object/blank.rb +15 -3
  77. data/lib/active_support/core_ext/object/deep_dup.rb +10 -3
  78. data/lib/active_support/core_ext/object/duplicable.rb +7 -12
  79. data/lib/active_support/core_ext/object/inclusion.rb +2 -2
  80. data/lib/active_support/core_ext/object/instance_variables.rb +1 -1
  81. data/lib/active_support/core_ext/object/json.rb +15 -7
  82. data/lib/active_support/core_ext/object/to_query.rb +1 -1
  83. data/lib/active_support/core_ext/object/try.rb +67 -21
  84. data/lib/active_support/core_ext/object/with_options.rb +1 -1
  85. data/lib/active_support/core_ext/object.rb +0 -1
  86. data/lib/active_support/core_ext/range/conversions.rb +18 -6
  87. data/lib/active_support/core_ext/range/each.rb +16 -18
  88. data/lib/active_support/core_ext/range/include_range.rb +20 -20
  89. data/lib/active_support/core_ext/securerandom.rb +23 -0
  90. data/lib/active_support/core_ext/string/behavior.rb +1 -1
  91. data/lib/active_support/core_ext/string/conversions.rb +3 -2
  92. data/lib/active_support/core_ext/string/filters.rb +1 -2
  93. data/lib/active_support/core_ext/string/inflections.rb +32 -5
  94. data/lib/active_support/core_ext/string/multibyte.rb +11 -7
  95. data/lib/active_support/core_ext/string/output_safety.rb +12 -14
  96. data/lib/active_support/core_ext/string/strip.rb +3 -6
  97. data/lib/active_support/core_ext/struct.rb +3 -6
  98. data/lib/active_support/core_ext/time/calculations.rb +18 -9
  99. data/lib/active_support/core_ext/time/conversions.rb +4 -2
  100. data/lib/active_support/core_ext/time/marshal.rb +2 -29
  101. data/lib/active_support/core_ext/time/zones.rb +36 -4
  102. data/lib/active_support/core_ext/time.rb +0 -1
  103. data/lib/active_support/core_ext/uri.rb +1 -3
  104. data/lib/active_support/core_ext.rb +2 -1
  105. data/lib/active_support/dependencies/interlock.rb +55 -0
  106. data/lib/active_support/dependencies.rb +88 -95
  107. data/lib/active_support/deprecation/behaviors.rb +15 -1
  108. data/lib/active_support/deprecation/instance_delegator.rb +13 -0
  109. data/lib/active_support/deprecation/method_wrappers.rb +42 -16
  110. data/lib/active_support/deprecation/proxy_wrappers.rb +47 -24
  111. data/lib/active_support/deprecation/reporting.rb +23 -5
  112. data/lib/active_support/deprecation.rb +1 -1
  113. data/lib/active_support/duration/iso8601_parser.rb +122 -0
  114. data/lib/active_support/duration/iso8601_serializer.rb +51 -0
  115. data/lib/active_support/duration.rb +90 -15
  116. data/lib/active_support/evented_file_update_checker.rb +199 -0
  117. data/lib/active_support/execution_wrapper.rb +126 -0
  118. data/lib/active_support/executor.rb +6 -0
  119. data/lib/active_support/file_update_checker.rb +23 -3
  120. data/lib/active_support/gem_version.rb +5 -5
  121. data/lib/active_support/gzip.rb +1 -1
  122. data/lib/active_support/hash_with_indifferent_access.rb +40 -11
  123. data/lib/active_support/i18n_railtie.rb +25 -4
  124. data/lib/active_support/inflector/inflections.rb +36 -5
  125. data/lib/active_support/inflector/methods.rb +97 -90
  126. data/lib/active_support/inflector/transliterate.rb +36 -21
  127. data/lib/active_support/json/decoding.rb +11 -10
  128. data/lib/active_support/json/encoding.rb +1 -51
  129. data/lib/active_support/key_generator.rb +7 -9
  130. data/lib/active_support/lazy_load_hooks.rb +46 -18
  131. data/lib/active_support/locale/en.yml +2 -0
  132. data/lib/active_support/log_subscriber/test_helper.rb +3 -3
  133. data/lib/active_support/log_subscriber.rb +1 -1
  134. data/lib/active_support/logger.rb +3 -4
  135. data/lib/active_support/logger_silence.rb +2 -1
  136. data/lib/active_support/logger_thread_safe_level.rb +2 -3
  137. data/lib/active_support/message_encryptor.rb +7 -7
  138. data/lib/active_support/message_verifier.rb +70 -8
  139. data/lib/active_support/multibyte/chars.rb +12 -3
  140. data/lib/active_support/multibyte/unicode.rb +44 -21
  141. data/lib/active_support/notifications/fanout.rb +5 -5
  142. data/lib/active_support/notifications/instrumenter.rb +20 -2
  143. data/lib/active_support/notifications.rb +2 -2
  144. data/lib/active_support/number_helper/number_to_currency_converter.rb +7 -9
  145. data/lib/active_support/number_helper/number_to_delimited_converter.rb +8 -3
  146. data/lib/active_support/number_helper/number_to_human_converter.rb +6 -4
  147. data/lib/active_support/number_helper/number_to_human_size_converter.rb +6 -2
  148. data/lib/active_support/number_helper/number_to_percentage_converter.rb +1 -1
  149. data/lib/active_support/number_helper/number_to_phone_converter.rb +11 -2
  150. data/lib/active_support/number_helper/number_to_rounded_converter.rb +30 -25
  151. data/lib/active_support/number_helper.rb +90 -67
  152. data/lib/active_support/ordered_hash.rb +1 -1
  153. data/lib/active_support/ordered_options.rb +15 -1
  154. data/lib/active_support/per_thread_registry.rb +3 -0
  155. data/lib/active_support/rails.rb +2 -2
  156. data/lib/active_support/railtie.rb +6 -1
  157. data/lib/active_support/reloader.rb +129 -0
  158. data/lib/active_support/rescuable.rb +101 -47
  159. data/lib/active_support/string_inquirer.rb +1 -1
  160. data/lib/active_support/subscriber.rb +5 -10
  161. data/lib/active_support/tagged_logging.rb +8 -7
  162. data/lib/active_support/test_case.rb +17 -29
  163. data/lib/active_support/testing/assertions.rb +15 -13
  164. data/lib/active_support/testing/deprecation.rb +9 -8
  165. data/lib/active_support/testing/file_fixtures.rb +34 -0
  166. data/lib/active_support/testing/isolation.rb +22 -8
  167. data/lib/active_support/testing/method_call_assertions.rb +41 -0
  168. data/lib/active_support/testing/stream.rb +42 -0
  169. data/lib/active_support/testing/time_helpers.rb +3 -1
  170. data/lib/active_support/time_with_zone.rb +123 -33
  171. data/lib/active_support/values/time_zone.rb +101 -47
  172. data/lib/active_support/values/unicode_tables.dat +0 -0
  173. data/lib/active_support/xml_mini/jdom.rb +1 -1
  174. data/lib/active_support/xml_mini/libxml.rb +2 -2
  175. data/lib/active_support/xml_mini/nokogiri.rb +2 -2
  176. data/lib/active_support.rb +11 -6
  177. metadata +36 -17
  178. data/lib/active_support/core_ext/big_decimal/yaml_conversions.rb +0 -16
  179. data/lib/active_support/core_ext/class/delegating_attributes.rb +0 -45
  180. data/lib/active_support/core_ext/date_time/zones.rb +0 -6
  181. data/lib/active_support/core_ext/object/itself.rb +0 -15
  182. data/lib/active_support/core_ext/thread.rb +0 -86
@@ -20,20 +20,22 @@ module ActiveSupport
20
20
 
21
21
  private
22
22
  def method_missing(called, *args, &block)
23
- warn caller, called, args
23
+ warn caller_locations, called, args
24
24
  target.__send__(called, *args, &block)
25
25
  end
26
26
  end
27
27
 
28
- # This DeprecatedObjectProxy transforms object to deprecated object.
28
+ # DeprecatedObjectProxy transforms an object into a deprecated one. It
29
+ # takes an object, a deprecation message and optionally a deprecator. The
30
+ # deprecator defaults to +ActiveSupport::Deprecator+ if none is specified.
29
31
  #
30
- # @old_object = DeprecatedObjectProxy.new(Object.new, "Don't use this object anymore!")
31
- # @old_object = DeprecatedObjectProxy.new(Object.new, "Don't use this object anymore!", deprecator_instance)
32
+ # deprecated_object = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(Object.new, "This object is now deprecated")
33
+ # # => #<Object:0x007fb9b34c34b0>
32
34
  #
33
- # When someone executes any method except +inspect+ on proxy object this will
34
- # trigger +warn+ method on +deprecator_instance+.
35
- #
36
- # Default deprecator is <tt>ActiveSupport::Deprecation</tt>
35
+ # deprecated_object.to_s
36
+ # DEPRECATION WARNING: This object is now deprecated.
37
+ # (Backtrace)
38
+ # # => "#<Object:0x007fb9b34c34b0>"
37
39
  class DeprecatedObjectProxy < DeprecationProxy
38
40
  def initialize(object, message, deprecator = ActiveSupport::Deprecation.instance)
39
41
  @object = object
@@ -51,13 +53,16 @@ module ActiveSupport
51
53
  end
52
54
  end
53
55
 
54
- # This DeprecatedInstanceVariableProxy transforms instance variable to
55
- # deprecated instance variable.
56
+ # DeprecatedInstanceVariableProxy transforms an instance variable into a
57
+ # deprecated one. It takes an instance of a class, a method on that class
58
+ # and an instance variable. It optionally takes a deprecator as the last
59
+ # argument. The deprecator defaults to +ActiveSupport::Deprecator+ if none
60
+ # is specified.
56
61
  #
57
62
  # class Example
58
- # def initialize(deprecator)
59
- # @request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request, :@request, deprecator)
60
- # @_request = :a_request
63
+ # def initialize
64
+ # @request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request, :@request)
65
+ # @_request = :special_request
61
66
  # end
62
67
  #
63
68
  # def request
@@ -69,12 +74,17 @@ module ActiveSupport
69
74
  # end
70
75
  # end
71
76
  #
72
- # When someone execute any method on @request variable this will trigger
73
- # +warn+ method on +deprecator_instance+ and will fetch <tt>@_request</tt>
74
- # variable via +request+ method and execute the same method on non-proxy
75
- # instance variable.
77
+ # example = Example.new
78
+ # # => #<Example:0x007fb9b31090b8 @_request=:special_request, @request=:special_request>
79
+ #
80
+ # example.old_request.to_s
81
+ # # => DEPRECATION WARNING: @request is deprecated! Call request.to_s instead of
82
+ # @request.to_s
83
+ # (Backtrace information…)
84
+ # "special_request"
76
85
  #
77
- # Default deprecator is <tt>ActiveSupport::Deprecation</tt>.
86
+ # example.request.to_s
87
+ # # => "special_request"
78
88
  class DeprecatedInstanceVariableProxy < DeprecationProxy
79
89
  def initialize(instance, method, var = "@#{method}", deprecator = ActiveSupport::Deprecation.instance)
80
90
  @instance = instance
@@ -93,15 +103,23 @@ module ActiveSupport
93
103
  end
94
104
  end
95
105
 
96
- # This DeprecatedConstantProxy transforms constant to deprecated constant.
106
+ # DeprecatedConstantProxy transforms a constant into a deprecated one. It
107
+ # takes the names of an old (deprecated) constant and of a new constant
108
+ # (both in string form) and optionally a deprecator. The deprecator defaults
109
+ # to +ActiveSupport::Deprecator+ if none is specified. The deprecated constant
110
+ # now returns the value of the new one.
111
+ #
112
+ # PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto)
97
113
  #
98
- # OLD_CONST = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('OLD_CONST', 'NEW_CONST')
99
- # OLD_CONST = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('OLD_CONST', 'NEW_CONST', deprecator_instance)
114
+ # (In a later update, the original implementation of `PLANETS` has been removed.)
100
115
  #
101
- # When someone use old constant this will trigger +warn+ method on
102
- # +deprecator_instance+.
116
+ # PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune)
117
+ # PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006')
103
118
  #
104
- # Default deprecator is <tt>ActiveSupport::Deprecation</tt>.
119
+ # PLANETS.map { |planet| planet.capitalize }
120
+ # # => DEPRECATION WARNING: PLANETS is deprecated! Use PLANETS_POST_2006 instead.
121
+ # (Backtrace information…)
122
+ # ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
105
123
  class DeprecatedConstantProxy < DeprecationProxy
106
124
  def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance)
107
125
  @old_const = old_const
@@ -109,6 +127,11 @@ module ActiveSupport
109
127
  @deprecator = deprecator
110
128
  end
111
129
 
130
+ # Returns the class of the new constant.
131
+ #
132
+ # PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune)
133
+ # PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006')
134
+ # PLANETS.class # => Array
112
135
  def class
113
136
  target.class
114
137
  end
@@ -1,3 +1,5 @@
1
+ require 'rbconfig'
2
+
1
3
  module ActiveSupport
2
4
  class Deprecation
3
5
  module Reporting
@@ -14,7 +16,7 @@ module ActiveSupport
14
16
  def warn(message = nil, callstack = nil)
15
17
  return if silenced
16
18
 
17
- callstack ||= caller(2)
19
+ callstack ||= caller_locations(2)
18
20
  deprecation_message(callstack, message).tap do |m|
19
21
  behavior.each { |b| b.call(m, callstack) }
20
22
  end
@@ -37,7 +39,7 @@ module ActiveSupport
37
39
  end
38
40
 
39
41
  def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = nil)
40
- caller_backtrace ||= caller(2)
42
+ caller_backtrace ||= caller_locations(2)
41
43
  deprecated_method_warning(deprecated_method_name, message).tap do |msg|
42
44
  warn(msg, caller_backtrace)
43
45
  end
@@ -63,7 +65,6 @@ module ActiveSupport
63
65
 
64
66
  def deprecation_message(callstack, message = nil)
65
67
  message ||= "You are using deprecated behavior which will be removed from the next major or minor release."
66
- message += '.' unless message =~ /\.$/
67
68
  "DEPRECATION WARNING: #{message} #{deprecation_caller_message(callstack)}"
68
69
  end
69
70
 
@@ -79,8 +80,19 @@ module ActiveSupport
79
80
  end
80
81
 
81
82
  def extract_callstack(callstack)
82
- rails_gem_root = File.expand_path("../../../../..", __FILE__) + "/"
83
- offending_line = callstack.find { |line| !line.start_with?(rails_gem_root) } || callstack.first
83
+ return _extract_callstack(callstack) if callstack.first.is_a? String
84
+
85
+ offending_line = callstack.find { |frame|
86
+ frame.absolute_path && !ignored_callstack(frame.absolute_path)
87
+ } || callstack.first
88
+
89
+ [offending_line.path, offending_line.lineno, offending_line.label]
90
+ end
91
+
92
+ def _extract_callstack(callstack)
93
+ warn "Please pass `caller_locations` to the deprecation API" if $VERBOSE
94
+ offending_line = callstack.find { |line| !ignored_callstack(line) } || callstack.first
95
+
84
96
  if offending_line
85
97
  if md = offending_line.match(/^(.+?):(\d+)(?::in `(.*?)')?/)
86
98
  md.captures
@@ -89,6 +101,12 @@ module ActiveSupport
89
101
  end
90
102
  end
91
103
  end
104
+
105
+ RAILS_GEM_ROOT = File.expand_path("../../../../..", __FILE__) + "/"
106
+
107
+ def ignored_callstack(path)
108
+ path.start_with?(RAILS_GEM_ROOT) || path.start_with?(RbConfig::CONFIG['rubylibdir'])
109
+ end
92
110
  end
93
111
  end
94
112
  end
@@ -32,7 +32,7 @@ module ActiveSupport
32
32
  # and the second is a library name
33
33
  #
34
34
  # ActiveSupport::Deprecation.new('2.0', 'MyLibrary')
35
- def initialize(deprecation_horizon = '5.0', gem_name = 'Rails')
35
+ def initialize(deprecation_horizon = '5.1', gem_name = 'Rails')
36
36
  self.gem_name = gem_name
37
37
  self.deprecation_horizon = deprecation_horizon
38
38
  # By default, warnings are not silenced and debugging is off.
@@ -0,0 +1,122 @@
1
+ require 'strscan'
2
+
3
+ module ActiveSupport
4
+ class Duration
5
+ # Parses a string formatted according to ISO 8601 Duration into the hash.
6
+ #
7
+ # See {ISO 8601}[http://en.wikipedia.org/wiki/ISO_8601#Durations] for more information.
8
+ #
9
+ # This parser allows negative parts to be present in pattern.
10
+ class ISO8601Parser # :nodoc:
11
+ class ParsingError < ::ArgumentError; end
12
+
13
+ PERIOD_OR_COMMA = /\.|,/
14
+ PERIOD = '.'.freeze
15
+ COMMA = ','.freeze
16
+
17
+ SIGN_MARKER = /\A\-|\+|/
18
+ DATE_MARKER = /P/
19
+ TIME_MARKER = /T/
20
+ DATE_COMPONENT = /(\-?\d+(?:[.,]\d+)?)(Y|M|D|W)/
21
+ TIME_COMPONENT = /(\-?\d+(?:[.,]\d+)?)(H|M|S)/
22
+
23
+ DATE_TO_PART = { 'Y' => :years, 'M' => :months, 'W' => :weeks, 'D' => :days }
24
+ TIME_TO_PART = { 'H' => :hours, 'M' => :minutes, 'S' => :seconds }
25
+
26
+ DATE_COMPONENTS = [:years, :months, :days]
27
+ TIME_COMPONENTS = [:hours, :minutes, :seconds]
28
+
29
+ attr_reader :parts, :scanner
30
+ attr_accessor :mode, :sign
31
+
32
+ def initialize(string)
33
+ @scanner = StringScanner.new(string)
34
+ @parts = {}
35
+ @mode = :start
36
+ @sign = 1
37
+ end
38
+
39
+ def parse!
40
+ while !finished?
41
+ case mode
42
+ when :start
43
+ if scan(SIGN_MARKER)
44
+ self.sign = (scanner.matched == '-') ? -1 : 1
45
+ self.mode = :sign
46
+ else
47
+ raise_parsing_error
48
+ end
49
+
50
+ when :sign
51
+ if scan(DATE_MARKER)
52
+ self.mode = :date
53
+ else
54
+ raise_parsing_error
55
+ end
56
+
57
+ when :date
58
+ if scan(TIME_MARKER)
59
+ self.mode = :time
60
+ elsif scan(DATE_COMPONENT)
61
+ parts[DATE_TO_PART[scanner[2]]] = number * sign
62
+ else
63
+ raise_parsing_error
64
+ end
65
+
66
+ when :time
67
+ if scan(TIME_COMPONENT)
68
+ parts[TIME_TO_PART[scanner[2]]] = number * sign
69
+ else
70
+ raise_parsing_error
71
+ end
72
+
73
+ end
74
+ end
75
+
76
+ validate!
77
+ parts
78
+ end
79
+
80
+ private
81
+
82
+ def finished?
83
+ scanner.eos?
84
+ end
85
+
86
+ # Parses number which can be a float with either comma or period.
87
+ def number
88
+ scanner[1] =~ PERIOD_OR_COMMA ? scanner[1].tr(COMMA, PERIOD).to_f : scanner[1].to_i
89
+ end
90
+
91
+ def scan(pattern)
92
+ scanner.scan(pattern)
93
+ end
94
+
95
+ def raise_parsing_error(reason = nil)
96
+ raise ParsingError, "Invalid ISO 8601 duration: #{scanner.string.inspect} #{reason}".strip
97
+ end
98
+
99
+ # Checks for various semantic errors as stated in ISO 8601 standard.
100
+ def validate!
101
+ raise_parsing_error('is empty duration') if parts.empty?
102
+
103
+ # Mixing any of Y, M, D with W is invalid.
104
+ if parts.key?(:weeks) && (parts.keys & DATE_COMPONENTS).any?
105
+ raise_parsing_error('mixing weeks with other date parts not allowed')
106
+ end
107
+
108
+ # Specifying an empty T part is invalid.
109
+ if mode == :time && (parts.keys & TIME_COMPONENTS).empty?
110
+ raise_parsing_error('time part marker is present but time part is empty')
111
+ end
112
+
113
+ fractions = parts.values.reject(&:zero?).select { |a| (a % 1) != 0 }
114
+ unless fractions.empty? || (fractions.size == 1 && fractions.last == @parts.values.reject(&:zero?).last)
115
+ raise_parsing_error '(only last part can be fractional)'
116
+ end
117
+
118
+ return true
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,51 @@
1
+ require 'active_support/core_ext/object/blank'
2
+ require 'active_support/core_ext/hash/transform_values'
3
+
4
+ module ActiveSupport
5
+ class Duration
6
+ # Serializes duration to string according to ISO 8601 Duration format.
7
+ class ISO8601Serializer
8
+ def initialize(duration, precision: nil)
9
+ @duration = duration
10
+ @precision = precision
11
+ end
12
+
13
+ # Builds and returns output string.
14
+ def serialize
15
+ output = 'P'
16
+ parts, sign = normalize
17
+ output << "#{parts[:years]}Y" if parts.key?(:years)
18
+ output << "#{parts[:months]}M" if parts.key?(:months)
19
+ output << "#{parts[:weeks]}W" if parts.key?(:weeks)
20
+ output << "#{parts[:days]}D" if parts.key?(:days)
21
+ time = ''
22
+ time << "#{parts[:hours]}H" if parts.key?(:hours)
23
+ time << "#{parts[:minutes]}M" if parts.key?(:minutes)
24
+ if parts.key?(:seconds)
25
+ time << "#{sprintf(@precision ? "%0.0#{@precision}f" : '%g', parts[:seconds])}S"
26
+ end
27
+ output << "T#{time}" if time.present?
28
+ "#{sign}#{output}"
29
+ end
30
+
31
+ private
32
+
33
+ # Return pair of duration's parts and whole duration sign.
34
+ # Parts are summarized (as they can become repetitive due to addition, etc).
35
+ # Zero parts are removed as not significant.
36
+ # If all parts are negative it will negate all of them and return minus as a sign.
37
+ def normalize
38
+ parts = @duration.parts.each_with_object(Hash.new(0)) do |(k,v),p|
39
+ p[k] += v unless v.zero?
40
+ end
41
+ # If all parts are negative - let's make a negative duration
42
+ sign = ''
43
+ if parts.values.all? { |v| v < 0 }
44
+ sign = '-'
45
+ parts.transform_values!(&:-@)
46
+ end
47
+ [parts, sign]
48
+ end
49
+ end
50
+ end
51
+ end
@@ -7,8 +7,82 @@ module ActiveSupport
7
7
  #
8
8
  # 1.month.ago # equivalent to Time.now.advance(months: -1)
9
9
  class Duration
10
+ SECONDS_PER_MINUTE = 60
11
+ SECONDS_PER_HOUR = 3600
12
+ SECONDS_PER_DAY = 86400
13
+ SECONDS_PER_WEEK = 604800
14
+ SECONDS_PER_MONTH = 2592000 # 30 days
15
+ SECONDS_PER_YEAR = 31557600 # length of a julian year (365.2425 days)
16
+
17
+ PARTS_IN_SECONDS = {
18
+ seconds: 1,
19
+ minutes: SECONDS_PER_MINUTE,
20
+ hours: SECONDS_PER_HOUR,
21
+ days: SECONDS_PER_DAY,
22
+ weeks: SECONDS_PER_WEEK,
23
+ months: SECONDS_PER_MONTH,
24
+ years: SECONDS_PER_YEAR
25
+ }.freeze
26
+
10
27
  attr_accessor :value, :parts
11
28
 
29
+ autoload :ISO8601Parser, 'active_support/duration/iso8601_parser'
30
+ autoload :ISO8601Serializer, 'active_support/duration/iso8601_serializer'
31
+
32
+ class << self
33
+ # Creates a new Duration from string formatted according to ISO 8601 Duration.
34
+ #
35
+ # See {ISO 8601}[http://en.wikipedia.org/wiki/ISO_8601#Durations] for more information.
36
+ # This method allows negative parts to be present in pattern.
37
+ # If invalid string is provided, it will raise +ActiveSupport::Duration::ISO8601Parser::ParsingError+.
38
+ def parse(iso8601duration)
39
+ parts = ISO8601Parser.new(iso8601duration).parse!
40
+ new(calculate_total_seconds(parts), parts)
41
+ end
42
+
43
+ def ===(other) #:nodoc:
44
+ other.is_a?(Duration)
45
+ rescue ::NoMethodError
46
+ false
47
+ end
48
+
49
+ def seconds(value) #:nodoc:
50
+ new(value, [[:seconds, value]])
51
+ end
52
+
53
+ def minutes(value) #:nodoc:
54
+ new(value * SECONDS_PER_MINUTE, [[:minutes, value]])
55
+ end
56
+
57
+ def hours(value) #:nodoc:
58
+ new(value * SECONDS_PER_HOUR, [[:hours, value]])
59
+ end
60
+
61
+ def days(value) #:nodoc:
62
+ new(value * SECONDS_PER_DAY, [[:days, value]])
63
+ end
64
+
65
+ def weeks(value) #:nodoc:
66
+ new(value * SECONDS_PER_WEEK, [[:weeks, value]])
67
+ end
68
+
69
+ def months(value) #:nodoc:
70
+ new(value * SECONDS_PER_MONTH, [[:months, value]])
71
+ end
72
+
73
+ def years(value) #:nodoc:
74
+ new(value * SECONDS_PER_YEAR, [[:years, value]])
75
+ end
76
+
77
+ private
78
+
79
+ def calculate_total_seconds(parts)
80
+ parts.inject(0) do |total, (part, value)|
81
+ total + value * PARTS_IN_SECONDS[part]
82
+ end
83
+ end
84
+ end
85
+
12
86
  def initialize(value, parts) #:nodoc:
13
87
  @value, @parts = value, parts
14
88
  end
@@ -52,6 +126,10 @@ module ActiveSupport
52
126
  end
53
127
  end
54
128
 
129
+ # Returns the amount of seconds a duration covers as a string.
130
+ # For more information check to_i method.
131
+ #
132
+ # 1.day.to_s # => "86400"
55
133
  def to_s
56
134
  @value.to_s
57
135
  end
@@ -90,12 +168,6 @@ module ActiveSupport
90
168
  @value.hash
91
169
  end
92
170
 
93
- def self.===(other) #:nodoc:
94
- other.is_a?(Duration)
95
- rescue ::NoMethodError
96
- false
97
- end
98
-
99
171
  # Calculates a new Time or Date that is as far in the future
100
172
  # as this Duration represents.
101
173
  def since(time = ::Time.current)
@@ -113,7 +185,7 @@ module ActiveSupport
113
185
  def inspect #:nodoc:
114
186
  parts.
115
187
  reduce(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }.
116
- sort_by {|unit, _ | [:years, :months, :days, :minutes, :seconds].index(unit)}.
188
+ sort_by {|unit, _ | [:years, :months, :weeks, :days, :hours, :minutes, :seconds].index(unit)}.
117
189
  map {|unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}"}.
118
190
  to_sentence(locale: ::I18n.default_locale)
119
191
  end
@@ -122,10 +194,16 @@ module ActiveSupport
122
194
  to_i
123
195
  end
124
196
 
125
- def respond_to_missing?(method, include_private=false) #:nodoc
197
+ def respond_to_missing?(method, include_private=false) #:nodoc:
126
198
  @value.respond_to?(method, include_private)
127
199
  end
128
200
 
201
+ # Build ISO 8601 Duration string for this duration.
202
+ # The +precision+ parameter can be used to limit seconds' precision of duration.
203
+ def iso8601(precision: nil)
204
+ ISO8601Serializer.new(self, precision: precision).serialize
205
+ end
206
+
129
207
  delegate :<=>, to: :value
130
208
 
131
209
  protected
@@ -135,6 +213,10 @@ module ActiveSupport
135
213
  if t.acts_like?(:time) || t.acts_like?(:date)
136
214
  if type == :seconds
137
215
  t.since(sign * number)
216
+ elsif type == :minutes
217
+ t.since(sign * number * 60)
218
+ elsif type == :hours
219
+ t.since(sign * number * 3600)
138
220
  else
139
221
  t.advance(type => sign * number)
140
222
  end
@@ -146,13 +228,6 @@ module ActiveSupport
146
228
 
147
229
  private
148
230
 
149
- # We define it as a workaround to Ruby 2.0.0-p353 bug.
150
- # For more information, check rails/rails#13055.
151
- # Remove it when we drop support for 2.0.0-p353.
152
- def ===(other) #:nodoc:
153
- value === other
154
- end
155
-
156
231
  def method_missing(method, *args, &block) #:nodoc:
157
232
  value.send(method, *args, &block)
158
233
  end