diff options
62 files changed, 2058 insertions, 385 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index ba66b525bc..2e7b3190fc 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,16 @@ ## Rails 4.0.0 (unreleased) ## +* change a way of ordering helpers from several directories. Previously, + when loading helpers from multiple paths, all of the helpers files were + gathered into one array an then they were sorted. Helpers from different + directories should not be mixed before loading them to make loading more + predictable. The most common use case for such behavior is loading helpers + from engines. When you load helpers from application and engine Foo, in + that order, first rails will load all of the helpers from application, + sorted alphabetically and then it will do the same for Foo engine. + + *Piotr Sarnacki* + * `truncate` now always returns an escaped HTMl-safe string. The option `:escape` can be used as false to not escape the result. diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 86d061e3b7..66cdfd40ff 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -95,9 +95,9 @@ module ActionController helpers = [] Array(path).each do |_path| extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ - helpers += Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } + names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } + helpers += names.sort end - helpers.sort! helpers.uniq! helpers end diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index dfc26acfad..8f97d1f014 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -1,8 +1,8 @@ # encoding: utf-8 -require 'active_support/core_ext/big_decimal/conversions' -require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/string/output_safety' +require 'active_support/number_helper' module ActionView # = Action View Number Helpers @@ -16,9 +16,6 @@ module ActionView # unchanged if can't be converted into a valid number. module NumberHelper - DEFAULT_CURRENCY_VALUES = { :format => "%u%n", :negative_format => "-%u%n", :unit => "$", :separator => ".", :delimiter => ",", - :precision => 2, :significant => false, :strip_insignificant_zeros => false } - # Raised when argument +number+ param given to the helpers is invalid and # the option :raise is set to +true+. class InvalidNumberError < StandardError @@ -63,25 +60,7 @@ module ActionView options = options.symbolize_keys parse_float(number, true) if options[:raise] - - number = number.to_s.strip - area_code = options[:area_code] - delimiter = options[:delimiter] || "-" - extension = options[:extension] - country_code = options[:country_code] - - if area_code - number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3") - else - number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") - number.slice!(0, 1) if number.start_with?(delimiter) && !delimiter.blank? - end - - str = '' - str << "+#{country_code}#{delimiter}" unless country_code.blank? - str << number - str << " x #{extension}" unless extension.blank? - ERB::Util.html_escape(str) + ERB::Util.html_escape(ActiveSupport::NumberHelper.number_to_phone(number, options)) end # Formats a +number+ into a currency string (e.g., $13.65). You @@ -128,34 +107,9 @@ module ActionView # # => 1234567890,50 £ def number_to_currency(number, options = {}) return unless number - options = options.symbolize_keys + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) - currency = translations_for('currency', options[:locale]) - currency[:negative_format] ||= "-" + currency[:format] if currency[:format] - - defaults = DEFAULT_CURRENCY_VALUES.merge(defaults_translations(options[:locale])).merge!(currency) - defaults[:negative_format] = "-" + options[:format] if options[:format] - options = defaults.merge!(options) - - unit = options.delete(:unit) - format = options.delete(:format) - - if number.to_f < 0 - format = options.delete(:negative_format) - number = number.respond_to?("abs") ? number.abs : number.sub(/^-/, '') - end - - begin - value = number_with_precision(number, options.merge(:raise => true)) - format.gsub('%n', value).gsub('%u', unit).html_safe - rescue InvalidNumberError => e - if options[:raise] - raise - else - formatted_number = format.gsub('%n', e.number).gsub('%u', unit) - e.number.to_s.html_safe? ? formatted_number.html_safe : formatted_number - end - end + wrap_with_output_safety_handling(number, options[:raise]){ ActiveSupport::NumberHelper.number_to_currency(number, options) } end # Formats a +number+ as a percentage string (e.g., 65%). You can @@ -196,24 +150,9 @@ module ActionView # number_to_percentage("98a", :raise => true) # => InvalidNumberError def number_to_percentage(number, options = {}) return unless number - options = options.symbolize_keys + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) - defaults = format_translations('percentage', options[:locale]) - options = defaults.merge!(options) - - format = options[:format] || "%n%" - - begin - value = number_with_precision(number, options.merge(:raise => true)) - format.gsub(/%n/, value).html_safe - rescue InvalidNumberError => e - if options[:raise] - raise - else - formatted_number = format.gsub(/%n/, e.number) - e.number.to_s.html_safe? ? formatted_number.html_safe : formatted_number - end - end + wrap_with_output_safety_handling(number, options[:raise]){ ActiveSupport::NumberHelper.number_to_percentage(number, options) } end # Formats a +number+ with grouped thousands using +delimiter+ @@ -246,15 +185,9 @@ module ActionView # # number_with_delimiter("112a", :raise => true) # => raise InvalidNumberError def number_with_delimiter(number, options = {}) - options = options.symbolize_keys + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) - parse_float(number, options[:raise]) or return number - - options = defaults_translations(options[:locale]).merge(options) - - parts = number.to_s.to_str.split('.') - parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}") - safe_join(parts, options[:separator]) + wrap_with_output_safety_handling(number, options[:raise]){ ActiveSupport::NumberHelper.number_to_delimited(number, options) } end # Formats a +number+ with the specified level of @@ -299,41 +232,11 @@ module ActionView # number_with_precision(1111.2345, :precision => 2, :separator => ',', :delimiter => '.') # # => 1.111,23 def number_with_precision(number, options = {}) - options = options.symbolize_keys + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) - number = (parse_float(number, options[:raise]) or return number) - - defaults = format_translations('precision', options[:locale]) - options = defaults.merge!(options) - - precision = options.delete :precision - significant = options.delete :significant - strip_insignificant_zeros = options.delete :strip_insignificant_zeros - - if significant and precision > 0 - if number == 0 - digits, rounded_number = 1, 0 - else - digits = (Math.log10(number.abs) + 1).floor - rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new((10 ** (digits - precision)).to_f.to_s)).round.to_f * 10 ** (digits - precision) - digits = (Math.log10(rounded_number.abs) + 1).floor # After rounding, the number of digits may have changed - end - precision -= digits - precision = precision > 0 ? precision : 0 #don't let it be negative - else - rounded_number = BigDecimal.new(number.to_s).round(precision).to_f - rounded_number = rounded_number.zero? ? rounded_number.abs : rounded_number #prevent showing negative zeros - end - formatted_number = number_with_delimiter("%01.#{precision}f" % rounded_number, options) - if strip_insignificant_zeros - escaped_separator = Regexp.escape(options[:separator]) - formatted_number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '').html_safe - else - formatted_number - end + wrap_with_output_safety_handling(number, options[:raise]){ ActiveSupport::NumberHelper.number_to_rounded(number, options) } end - STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb].freeze # Formats the bytes in +number+ into a more understandable # representation (e.g., giving it 1500 yields 1.5 KB). This @@ -383,40 +286,11 @@ module ActionView # number_to_human_size(1234567890123, :precision => 5) # => "1.1229 TB" # number_to_human_size(524288000, :precision => 5) # => "500 MB" def number_to_human_size(number, options = {}) - options = options.symbolize_keys - - number = (parse_float(number, options[:raise]) or return number) - - defaults = format_translations('human', options[:locale]) - options = defaults.merge!(options) - - #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files - options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) - - storage_units_format = I18n.translate(:'number.human.storage_units.format', :locale => options[:locale], :raise => true) + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) - base = options[:prefix] == :si ? 1000 : 1024 - - if number.to_i < base - unit = I18n.translate(:'number.human.storage_units.units.byte', :locale => options[:locale], :count => number.to_i, :raise => true) - storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit).html_safe - else - max_exp = STORAGE_UNITS.size - 1 - exponent = (Math.log(number) / Math.log(base)).to_i # Convert to base - exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit - number /= base ** exponent - - unit_key = STORAGE_UNITS[exponent] - unit = I18n.translate(:"number.human.storage_units.units.#{unit_key}", :locale => options[:locale], :count => number, :raise => true) - - formatted_number = number_with_precision(number, options) - storage_units_format.gsub(/%n/, formatted_number).gsub(/%u/, unit).html_safe - end + wrap_with_output_safety_handling(number, options[:raise]){ ActiveSupport::NumberHelper.number_to_human_size(number, options) } end - DECIMAL_UNITS = {0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion, - -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto}.freeze - # Pretty prints (formats and approximates) a number in a way it # is more readable by humans (eg.: 1200000000 becomes "1.2 # Billion"). This is useful for numbers that can get very large @@ -516,60 +390,34 @@ module ActionView # number_to_human(0.34, :units => :distance) # => "34 centimeters" # def number_to_human(number, options = {}) - options = options.symbolize_keys + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) - number = (parse_float(number, options[:raise]) or return number) + wrap_with_output_safety_handling(number, options[:raise]){ ActiveSupport::NumberHelper.number_to_human(number, options) } + end - defaults = format_translations('human', options[:locale]) - options = defaults.merge!(options) + private - #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files - options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) + def escape_unsafe_delimiters_and_separators(options) + options[:separator] = ERB::Util.html_escape(options[:separator]) if options[:separator] && !options[:separator].html_safe? + options[:delimiter] = ERB::Util.html_escape(options[:delimiter]) if options[:delimiter] && !options[:delimiter].html_safe? + options + end - inverted_du = DECIMAL_UNITS.invert + def wrap_with_output_safety_handling(number, raise_on_invalid, &block) + valid_float = valid_float?(number) + raise InvalidNumberError, number if raise_on_invalid && !valid_float - units = options.delete :units - unit_exponents = case units - when Hash - units - when String, Symbol - I18n.translate(:"#{units}", :locale => options[:locale], :raise => true) - when nil - I18n.translate(:"number.human.decimal_units.units", :locale => options[:locale], :raise => true) - else - raise ArgumentError, ":units must be a Hash or String translation scope." - end.keys.map{|e_name| inverted_du[e_name] }.sort_by{|e| -e} + formatted_number = yield - number_exponent = number != 0 ? Math.log10(number.abs).floor : 0 - display_exponent = unit_exponents.find{ |e| number_exponent >= e } || 0 - number /= 10 ** display_exponent - - unit = case units - when Hash - units[DECIMAL_UNITS[display_exponent]] - when String, Symbol - I18n.translate(:"#{units}.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i) + if valid_float || number.html_safe? + formatted_number.html_safe else - I18n.translate(:"number.human.decimal_units.units.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i) + formatted_number end - - decimal_format = options[:format] || I18n.translate(:'number.human.decimal_units.format', :locale => options[:locale], :default => "%n %u") - formatted_number = number_with_precision(number, options) - decimal_format.gsub(/%n/, formatted_number).gsub(/%u/, unit).strip.html_safe - end - - private - - def format_translations(namespace, locale) - defaults_translations(locale).merge(translations_for(namespace, locale)) - end - - def defaults_translations(locale) - I18n.translate(:'number.format', :locale => locale, :default => {}) end - def translations_for(namespace, locale) - I18n.translate(:"number.#{namespace}.format", :locale => locale, :default => {}) + def valid_float?(number) + !parse_float(number, false).nil? end def parse_float(number, raise_error) diff --git a/actionpack/lib/action_view/locale/en.yml b/actionpack/lib/action_view/locale/en.yml index 8e9db634fb..8a56f147b8 100644 --- a/actionpack/lib/action_view/locale/en.yml +++ b/actionpack/lib/action_view/locale/en.yml @@ -1,102 +1,4 @@ "en": - number: - # Used in number_with_delimiter() - # These are also the defaults for 'currency', 'percentage', 'precision', and 'human' - format: - # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5) - separator: "." - # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three) - delimiter: "," - # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00) - precision: 3 - # If set to true, precision will mean the number of significant digits instead - # of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2) - significant: false - # If set, the zeros after the decimal separator will always be stripped (eg.: 1.200 will be 1.2) - strip_insignificant_zeros: false - - # Used in number_to_currency() - currency: - format: - # Where is the currency sign? %u is the currency unit, %n the number (default: $5.00) - format: "%u%n" - unit: "$" - # These five are to override number.format and are optional - separator: "." - delimiter: "," - precision: 2 - significant: false - strip_insignificant_zeros: false - - # Used in number_to_percentage() - percentage: - format: - # These five are to override number.format and are optional - # separator: - delimiter: "" - # precision: - # significant: false - # strip_insignificant_zeros: false - format: "%n%" - - # Used in number_to_precision() - precision: - format: - # These five are to override number.format and are optional - # separator: - delimiter: "" - # precision: - # significant: false - # strip_insignificant_zeros: false - - # Used in number_to_human_size() and number_to_human() - human: - format: - # These five are to override number.format and are optional - # separator: - delimiter: "" - precision: 3 - significant: true - strip_insignificant_zeros: true - # Used in number_to_human_size() - storage_units: - # Storage units output formatting. - # %u is the storage unit, %n is the number (default: 2 MB) - format: "%n %u" - units: - byte: - one: "Byte" - other: "Bytes" - kb: "KB" - mb: "MB" - gb: "GB" - tb: "TB" - # Used in number_to_human() - decimal_units: - format: "%n %u" - # Decimal units output formatting - # By default we will only quantify some of the exponents - # but the commented ones might be defined or overridden - # by the user. - units: - # femto: Quadrillionth - # pico: Trillionth - # nano: Billionth - # micro: Millionth - # mili: Thousandth - # centi: Hundredth - # deci: Tenth - unit: "" - # ten: - # one: Ten - # other: Tens - # hundred: Hundred - thousand: Thousand - million: Million - billion: Billion - trillion: Trillion - quadrillion: Quadrillion - # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words() datetime: distance_in_words: diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb index 757661d8d0..deb234b04f 100644 --- a/actionpack/test/controller/helper_test.rb +++ b/actionpack/test/controller/helper_test.rb @@ -46,12 +46,42 @@ end class MeTooController < JustMeController end +class HelpersPathsController < ActionController::Base + paths = ["helpers2_pack", "helpers1_pack"].map do |path| + File.join(File.expand_path('../../fixtures', __FILE__), path) + end + $:.unshift(*paths) + + self.helpers_path = paths + helper :all + + def index + render :inline => "<%= conflicting_helper %>" + end +end + module LocalAbcHelper def a() end def b() end def c() end end +class HelperPathsTest < ActiveSupport::TestCase + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_helpers_paths_priority + request = ActionController::TestRequest.new + responses = HelpersPathsController.action(:index).call(request.env) + + # helpers1_pack was given as a second path, so pack1_helper should be + # included as the second one + assert_equal "pack1", responses.last.body + end +end + class HelperTest < ActiveSupport::TestCase class TestController < ActionController::Base attr_accessor :delegate_attr diff --git a/actionpack/test/fixtures/helpers1_pack/pack1_helper.rb b/actionpack/test/fixtures/helpers1_pack/pack1_helper.rb new file mode 100644 index 0000000000..9faa427736 --- /dev/null +++ b/actionpack/test/fixtures/helpers1_pack/pack1_helper.rb @@ -0,0 +1,5 @@ +module Pack1Helper + def conflicting_helper + "pack1" + end +end diff --git a/actionpack/test/fixtures/helpers2_pack/pack2_helper.rb b/actionpack/test/fixtures/helpers2_pack/pack2_helper.rb new file mode 100644 index 0000000000..cf56697dfb --- /dev/null +++ b/actionpack/test/fixtures/helpers2_pack/pack2_helper.rb @@ -0,0 +1,5 @@ +module Pack2Helper + def conflicting_helper + "pack2" + end +end diff --git a/actionpack/test/template/number_helper_test.rb b/actionpack/test/template/number_helper_test.rb index 14ca6d9879..057cb47f53 100644 --- a/actionpack/test/template/number_helper_test.rb +++ b/actionpack/test/template/number_helper_test.rb @@ -33,6 +33,7 @@ class NumberHelperTest < ActionView::TestCase assert_equal("+18005551212", number_to_phone(8005551212, :country_code => 1, :delimiter => '')) assert_equal("22-555-1212", number_to_phone(225551212)) assert_equal("+45-22-555-1212", number_to_phone(225551212, :country_code => 45)) + assert_equal '111<script></script>111<script></script>1111', number_to_phone(1111111111, :delimiter => "<script></script>") end def test_number_to_currency @@ -47,6 +48,8 @@ class NumberHelperTest < ActionView::TestCase assert_equal("$1,234,567,890.50", number_to_currency("1234567890.50")) assert_equal("1,234,567,890.50 Kč", number_to_currency("1234567890.50", {:unit => "Kč", :format => "%n %u"})) assert_equal("1,234,567,890.50 - Kč", number_to_currency("-1234567890.50", {:unit => "Kč", :format => "%n %u", :negative_format => "%n - %u"})) + assert_equal '$1<script></script>01', number_to_currency(1.01, :separator => "<script></script>") + assert_equal '$1<script></script>000.00', number_to_currency(1000, :delimiter => "<script></script>") end def test_number_to_percentage @@ -58,6 +61,8 @@ class NumberHelperTest < ActionView::TestCase assert_equal("123.4%", number_to_percentage(123.400, :precision => 3, :strip_insignificant_zeros => true)) assert_equal("1.000,000%", number_to_percentage(1000, :delimiter => '.', :separator => ',')) assert_equal("1000.000 %", number_to_percentage(1000, :format => "%n %")) + assert_equal '1<script></script>010%', number_to_percentage(1.01, :separator => "<script></script>") + assert_equal '1<script></script>000.000%', number_to_percentage(1000, :delimiter => "<script></script>") end def test_number_with_delimiter @@ -104,6 +109,8 @@ class NumberHelperTest < ActionView::TestCase def test_number_with_precision_with_custom_delimiter_and_separator assert_equal '31,83', number_with_precision(31.825, :precision => 2, :separator => ',') assert_equal '1.231,83', number_with_precision(1231.825, :precision => 2, :separator => ',', :delimiter => '.') + assert_equal '1<script></script>010', number_with_precision(1.01, :separator => "<script></script>") + assert_equal '1<script></script>000.000', number_with_precision(1000, :delimiter => "<script></script>") end def test_number_with_precision_with_significant_digits @@ -193,6 +200,7 @@ class NumberHelperTest < ActionView::TestCase assert_equal '1.0 KB', number_to_human_size(kilobytes(1.0123), :precision => 2, :strip_insignificant_zeros => false) assert_equal '1.012 KB', number_to_human_size(kilobytes(1.0123), :precision => 3, :significant => false) assert_equal '1 KB', number_to_human_size(kilobytes(1.0123), :precision => 0, :significant => true) #ignores significant it precision is 0 + assert_equal '9<script></script>86 KB', number_to_human_size(10100, :separator => "<script></script>") end def test_number_to_human_size_with_custom_delimiter_and_separator @@ -253,6 +261,9 @@ class NumberHelperTest < ActionView::TestCase #Spaces are stripped from the resulting string assert_equal '4', number_to_human(4, :units => {:unit => "", :ten => 'tens '}) assert_equal '4.5 tens', number_to_human(45, :units => {:unit => "", :ten => ' tens '}) + + assert_equal '1<script></script>01', number_to_human(1.01, :separator => "<script></script>") + assert_equal '100<script></script>000 Quadrillion', number_to_human(10**20, :delimiter => "<script></script>") end def test_number_to_human_with_custom_format diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 789cff0673..eb34edb91b 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,5 +1,7 @@ ## Rails 4.0.0 (unreleased) ## +* Passing false hash values to `validates` will no longer enable the corresponding validators *Steve Purcell* + * `ConfirmationValidator` error messages will attach to `:#{attribute}_confirmation` instead of `attribute` *Brian Cardarella* * Added ActiveModel::Model, a mixin to make Ruby objects work with AP out of box *Guillermo Iguaran* diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 6f0ca92e2a..7a86701f73 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -42,9 +42,9 @@ module ActiveModel # Specify +options+ with additional translating options. def human_attribute_name(attribute, options = {}) options = { :count => 1 }.merge!(options) - parts = attribute.to_s.split(".", 2) + parts = attribute.to_s.split(".") attribute = parts.pop - namespace = parts.pop + namespace = parts.join("/") unless parts.empty? attributes_scope = "#{self.i18n_scope}.attributes" if namespace diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index d94c4e3f4f..6c13d2b4a2 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -88,6 +88,7 @@ module ActiveModel defaults.merge!(:attributes => attributes) validations.each do |key, options| + next unless options key = "#{key.to_s.camelize}Validator" begin diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb index 4999583802..fd833cdd06 100644 --- a/activemodel/test/cases/translation_test.rb +++ b/activemodel/test/cases/translation_test.rb @@ -56,6 +56,11 @@ class ActiveModelI18nTests < ActiveModel::TestCase assert_equal 'person gender attribute', Person::Gender.human_attribute_name('attribute') end + def test_translated_deeply_nested_model_attributes + I18n.backend.store_translations 'en', :activemodel => {:attributes => {:"person/contacts/addresses" => {:street => 'Deeply Nested Address Street'}}} + assert_equal 'Deeply Nested Address Street', Person.human_attribute_name('contacts.addresses.street') + end + def test_translated_nested_model_attributes I18n.backend.store_translations 'en', :activemodel => {:attributes => {:"person/addresses" => {:street => 'Person Address Street'}}} assert_equal 'Person Address Street', Person.human_attribute_name('addresses.street') diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index 1f5023bf76..8ea9745fbf 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -330,6 +330,11 @@ class ValidationsTest < ActiveModel::TestCase end end + def test_validates_with_false_hash_value + Topic.validates :title, :presence => false + assert Topic.new.valid? + end + def test_strict_validation_error_message Topic.validates :title, :strict => true, :presence => true diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 8f1f315e42..4f4e087acd 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,25 @@ ## Rails 4.0.0 (unreleased) ## +* Allow blocks for `count` with `ActiveRecord::Relation`, to work similar as + `Array#count`: + + Person.where("age > 26").count { |person| gender == 'female' } + + *Chris Finne & Carlos Antonio da Silva* + +* Added support to `CollectionAssociation#delete` for passing `fixnum` + or `string` values as record ids. This finds the records responding + to the `id` and executes delete on them. + + class Person < ActiveRecord::Base + has_many :pets + end + + person.pets.delete("1") # => [#<Pet id: 1>] + person.pets.delete(2, 3) # => [#<Pet id: 2>, #<Pet id: 3>] + + *Francesco Rodriguez* + * Deprecated most of the 'dynamic finder' methods. All dynamic methods except for `find_by_...` and `find_by_...!` are deprecated. Here's how you can rewrite the code: diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 210820062b..f8526bb691 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -80,6 +80,7 @@ module ActiveRecord autoload :Sanitization autoload :Schema autoload :SchemaDumper + autoload :SchemaMigration autoload :Scoping autoload :Serialization autoload :SessionStore diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 4ec176e641..e94fe35170 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -230,6 +230,7 @@ module ActiveRecord delete_records(:all, dependent) end else + records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } delete_or_destroy(records, dependent) end end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index f6183f8527..2fb80fdc4c 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -469,6 +469,8 @@ module ActiveRecord # # :call-seq: # delete(*records) + # delete(*fixnum_ids) + # delete(*string_ids) # # Deletes the +records+ supplied and removes them from the collection. For # +has_many+ associations, the deletion is done according to the strategy @@ -560,6 +562,30 @@ module ActiveRecord # # Pet.find(1) # # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=1 + # + # You can pass +Fixnum+ or +String+ values, it finds the records + # responding to the +id+ and executes delete on them. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete("1") + # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>] + # + # person.pets.delete(2, 3) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] ## # :method: destroy diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 62b0f51bb2..5758ac4569 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1,5 +1,4 @@ require 'active_support/deprecation/reporting' -require 'active_record/schema_migration' require 'active_record/migration/join_table' module ActiveRecord diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 2a9139749d..ac4f53c774 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,7 +1,6 @@ require "active_support/core_ext/module/delegation" require "active_support/core_ext/class/attribute_accessors" require 'active_support/deprecation' -require 'active_record/schema_migration' require 'set' module ActiveRecord diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index eb2769f1ef..1e497b2a79 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -30,6 +30,7 @@ module ActiveRecord ) rake_tasks do + require "active_record/base" load "active_record/railties/databases.rake" end @@ -38,10 +39,15 @@ module ActiveRecord # first time. Also, make it output to STDERR. console do |app| require "active_record/railties/console_sandbox" if app.sandbox? + require "active_record/base" console = ActiveSupport::Logger.new(STDERR) Rails.logger.extend ActiveSupport::Logger.broadcast console end + runner do |app| + require "active_record/base" + end + initializer "active_record.initialize_timezone" do ActiveSupport.on_load(:active_record) do self.time_zone_aware_attributes = true diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index ab32bfe70a..ad49c80e4f 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -16,9 +16,16 @@ module ActiveRecord # # Person.count(:age, distinct: true) # # => counts the number of different age values + # + # Person.where("age > 26").count { |person| gender == 'female' } + # # => queries people where "age > 26" then count the loaded results filtering by gender def count(column_name = nil, options = {}) - column_name, options = nil, column_name if column_name.is_a?(Hash) - calculate(:count, column_name, options) + if block_given? + self.to_a.count { |item| yield item } + else + column_name, options = nil, column_name if column_name.is_a?(Hash) + calculate(:count, column_name, options) + end end # Calculates the average value on a given column. Returns +nil+ if there's @@ -52,9 +59,13 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.sum('age') # => 4562 + # # => returns the total sum of all people's age + # + # Person.where('age > 100').sum { |person| person.age - 100 } + # # queries people where "age > 100" then perform a sum calculation with the block returns def sum(*args) if block_given? - self.to_a.sum(*args) {|*block_args| yield(*block_args)} + self.to_a.sum(*args) { |item| yield item } else calculate(:sum, *args) end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 8b384c2513..0d8f311117 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -936,10 +936,24 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 2, summit.client_of end - def test_deleting_type_mismatch + def test_deleting_by_fixnum_id david = Developer.find(1) - david.projects.reload - assert_raise(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(1) } + + assert_difference 'david.projects.count', -1 do + assert_equal 1, david.projects.delete(1).size + end + + assert_equal 1, david.projects.size + end + + def test_deleting_by_string_id + david = Developer.find(1) + + assert_difference 'david.projects.count', -1 do + assert_equal 1, david.projects.delete('1').size + end + + assert_equal 1, david.projects.size end def test_deleting_self_type_mismatch diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index ecc676f300..783b83631c 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -578,7 +578,27 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_deleting_junk_from_has_many_through_should_raise_type_mismatch - assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:thinking).tags.delete("Uhh what now?") } + assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:thinking).tags.delete(Object.new) } + end + + def test_deleting_by_fixnum_id_from_has_many_through + post = posts(:thinking) + + assert_difference 'post.tags.count', -1 do + assert_equal 1, post.tags.delete(1).size + end + + assert_equal 0, post.tags.size + end + + def test_deleting_by_string_id_from_has_many_through + post = posts(:thinking) + + assert_difference 'post.tags.count', -1 do + assert_equal 1, post.tags.delete('1').size + end + + assert_equal 0, post.tags.size end def test_has_many_through_sum_uses_calculations diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 041f8ffb7c..a279b0e77c 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -376,6 +376,22 @@ class CalculationsTest < ActiveRecord::TestCase Company.where(:type => "Firm").from('companies').count(:type) end + def test_count_with_block_acts_as_array + accounts = Account.where('id > 0') + assert_equal Account.count, accounts.count { true } + assert_equal 0, accounts.count { false } + assert_equal Account.where('credit_limit > 50').size, accounts.count { |account| account.credit_limit > 50 } + assert_equal Account.count, Account.count { true } + assert_equal 0, Account.count { false } + end + + def test_sum_with_block_acts_as_array + accounts = Account.where('id > 0') + assert_equal Account.sum(:credit_limit), accounts.sum { |account| account.credit_limit } + assert_equal Account.sum(:credit_limit) + Account.count, accounts.sum{ |account| account.credit_limit + 1 } + assert_equal 0, accounts.sum { |account| 0 } + end + def test_sum_with_from_option assert_equal Account.sum(:credit_limit), Account.from('accounts').sum(:credit_limit) assert_equal Account.where("credit_limit > 50").sum(:credit_limit), diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 62b8a789c7..a818ef0c5d 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,5 +1,9 @@ ## Rails 4.0.0 (unreleased) ## +* ActionView::Helpers::NumberHelper methods have been moved to ActiveSupport::NumberHelper and are now available via + Numeric#to_s. Numeric#to_s now accepts the formatting options :phone, :currency, :percentage, :delimited, + :rounded, :human, and :human_size. *Andrew Mutz* + * Add `Hash#transform_keys`, `Hash#transform_keys!`, `Hash#deep_transform_keys`, and `Hash#deep_transform_keys!`. *Mark McSpadden* * Changed xml type `datetime` to `dateTime` (with upper case letter `T`). *Angelo Capilleri* diff --git a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb index 3ec7e576c8..5dc5710c53 100644 --- a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb +++ b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb @@ -17,8 +17,13 @@ class BigDecimal end DEFAULT_STRING_FORMAT = 'F' - def to_formatted_s(format = DEFAULT_STRING_FORMAT) - _original_to_s(format) + def to_formatted_s(*args) + if args[0].is_a?(Symbol) + super + else + format = args[0] || DEFAULT_STRING_FORMAT + _original_to_s(format) + end end alias_method :_original_to_s, :to_s alias_method :to_s, :to_formatted_s diff --git a/activesupport/lib/active_support/core_ext/numeric.rb b/activesupport/lib/active_support/core_ext/numeric.rb index 3805cf7990..a6bc0624be 100644 --- a/activesupport/lib/active_support/core_ext/numeric.rb +++ b/activesupport/lib/active_support/core_ext/numeric.rb @@ -1,2 +1,3 @@ require 'active_support/core_ext/numeric/bytes' require 'active_support/core_ext/numeric/time' +require 'active_support/core_ext/numeric/conversions' diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb new file mode 100644 index 0000000000..2bbfa78639 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb @@ -0,0 +1,135 @@ +require 'active_support/core_ext/big_decimal/conversions' +require 'active_support/number_helper' + +class Numeric + + # Provides options for converting numbers into formatted strings. + # Options are provided for phone numbers, currency, percentage, + # precision, positional notation, file size and pretty printing. + # + # ==== Options + # + # For details on which formats use which options, see ActiveSupport::NumberHelper + # + # ==== Examples + # + # Phone Numbers: + # 5551234.to_s(:phone) # => 555-1234 + # 1235551234.to_s(:phone) # => 123-555-1234 + # 1235551234.to_s(:phone, :area_code => true) # => (123) 555-1234 + # 1235551234.to_s(:phone, :delimiter => " ") # => 123 555 1234 + # 1235551234.to_s(:phone, :area_code => true, :extension => 555) # => (123) 555-1234 x 555 + # 1235551234.to_s(:phone, :country_code => 1) # => +1-123-555-1234 + # 1235551234.to_s(:phone, :country_code => 1, :extension => 1343, :delimiter => ".") + # # => +1.123.555.1234 x 1343 + # + # Currency: + # 1234567890.50.to_s(:currency) # => $1,234,567,890.50 + # 1234567890.506.to_s(:currency) # => $1,234,567,890.51 + # 1234567890.506.to_s(:currency, :precision => 3) # => $1,234,567,890.506 + # 1234567890.506.to_s(:currency, :locale => :fr) # => 1 234 567 890,51 € + # -1234567890.50.to_s(:currency, :negative_format => "(%u%n)") + # # => ($1,234,567,890.50) + # 1234567890.50.to_s(:currency, :unit => "£", :separator => ",", :delimiter => "") + # # => £1234567890,50 + # 1234567890.50.to_s(:currency, :unit => "£", :separator => ",", :delimiter => "", :format => "%n %u") + # # => 1234567890,50 £ + # + # Percentage: + # 100.to_s(:percentage) # => 100.000% + # 100.to_s(:percentage, :precision => 0) # => 100% + # 1000.to_s(:percentage, :delimiter => '.', :separator => ',') # => 1.000,000% + # 302.24398923423.to_s(:percentage, :precision => 5) # => 302.24399% + # 1000.to_s(:percentage, :locale => :fr) # => 1 000,000% + # 100.to_s(:percentage, :format => "%n %") # => 100 % + # + # Delimited: + # 12345678.to_s(:delimited) # => 12,345,678 + # 12345678.05.to_s(:delimited) # => 12,345,678.05 + # 12345678.to_s(:delimited, :delimiter => ".") # => 12.345.678 + # 12345678.to_s(:delimited, :delimiter => ",") # => 12,345,678 + # 12345678.05.to_s(:delimited, :separator => " ") # => 12,345,678 05 + # 12345678.05.to_s(:delimited, :locale => :fr) # => 12 345 678,05 + # 98765432.98.to_s(:delimited, :delimiter => " ", :separator => ",") + # # => 98 765 432,98 + # + # Rounded: + # 111.2345.to_s(:rounded) # => 111.235 + # 111.2345.to_s(:rounded, :precision => 2) # => 111.23 + # 13.to_s(:rounded, :precision => 5) # => 13.00000 + # 389.32314.to_s(:rounded, :precision => 0) # => 389 + # 111.2345.to_s(:rounded, :significant => true) # => 111 + # 111.2345.to_s(:rounded, :precision => 1, :significant => true) # => 100 + # 13.to_s(:rounded, :precision => 5, :significant => true) # => 13.000 + # 111.234.to_s(:rounded, :locale => :fr) # => 111,234 + # 13.to_s(:rounded, :precision => 5, :significant => true, :strip_insignificant_zeros => true) + # # => 13 + # 389.32314.to_s(:rounded, :precision => 4, :significant => true) # => 389.3 + # 1111.2345.to_s(:rounded, :precision => 2, :separator => ',', :delimiter => '.') + # # => 1.111,23 + # + # Human-friendly size in Bytes: + # 123.to_s(:human_size) # => 123 Bytes + # 1234.to_s(:human_size) # => 1.21 KB + # 12345.to_s(:human_size) # => 12.1 KB + # 1234567.to_s(:human_size) # => 1.18 MB + # 1234567890.to_s(:human_size) # => 1.15 GB + # 1234567890123.to_s(:human_size) # => 1.12 TB + # 1234567.to_s(:human_size, :precision => 2) # => 1.2 MB + # 483989.to_s(:human_size, :precision => 2) # => 470 KB + # 1234567.to_s(:human_size, :precision => 2, :separator => ',') # => 1,2 MB + # 1234567890123.to_s(:human_size, :precision => 5) # => "1.1229 TB" + # 524288000.to_s(:human_size, :precision => 5) # => "500 MB" + # + # Human-friendly format: + # 123.to_s(:human) # => "123" + # 1234.to_s(:human) # => "1.23 Thousand" + # 12345.to_s(:human) # => "12.3 Thousand" + # 1234567.to_s(:human) # => "1.23 Million" + # 1234567890.to_s(:human) # => "1.23 Billion" + # 1234567890123.to_s(:human) # => "1.23 Trillion" + # 1234567890123456.to_s(:human) # => "1.23 Quadrillion" + # 1234567890123456789.to_s(:human) # => "1230 Quadrillion" + # 489939.to_s(:human, :precision => 2) # => "490 Thousand" + # 489939.to_s(:human, :precision => 4) # => "489.9 Thousand" + # 1234567.to_s(:human, :precision => 4, + # :significant => false) # => "1.2346 Million" + # 1234567.to_s(:human, :precision => 1, + # :separator => ',', + # :significant => false) # => "1,2 Million" + def to_formatted_s(format = :default, options = {}) + case format + when :phone + return ActiveSupport::NumberHelper.number_to_phone(self, options) + when :currency + return ActiveSupport::NumberHelper.number_to_currency(self, options) + when :percentage + return ActiveSupport::NumberHelper.number_to_percentage(self, options) + when :delimited + return ActiveSupport::NumberHelper.number_to_delimited(self, options) + when :rounded + return ActiveSupport::NumberHelper.number_to_rounded(self, options) + when :human + return ActiveSupport::NumberHelper.number_to_human(self, options) + when :human_size + return ActiveSupport::NumberHelper.number_to_human_size(self, options) + else + self.to_default_s + end + end + + [Float, Fixnum, Bignum, BigDecimal].each do |klass| + klass.send(:alias_method, :to_default_s, :to_s) + + klass.send(:define_method, :to_s) do |*args| + if args[0].is_a?(Symbol) + format = args[0] + options = args[1] || {} + + self.to_formatted_s(format, options) + else + to_default_s(*args) + end + end + end +end diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb index 5c32a2453d..8fa8157d65 100644 --- a/activesupport/lib/active_support/core_ext/string/access.rb +++ b/activesupport/lib/active_support/core_ext/string/access.rb @@ -1,5 +1,3 @@ -require 'active_support/multibyte' - class String # If you pass a single Fixnum, returns a substring of one character at that # position. The first character of the string is at position 0, the next at diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb index 70f2dcb562..8644529806 100644 --- a/activesupport/lib/active_support/core_ext/string/filters.rb +++ b/activesupport/lib/active_support/core_ext/string/filters.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/string/multibyte' - class String # Returns the string, first removing all whitespace on both ends of # the string, and then changing remaining consecutive whitespace diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 6a4ddb1602..ded02c873a 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -162,18 +162,18 @@ class Struct #:nodoc: end class TrueClass - AS_JSON = ActiveSupport::JSON::Variable.new('true').freeze - def as_json(options = nil) AS_JSON end #:nodoc: + def as_json(options = nil) self end #:nodoc: + def encode_json(encoder) to_s end #:nodoc: end class FalseClass - AS_JSON = ActiveSupport::JSON::Variable.new('false').freeze - def as_json(options = nil) AS_JSON end #:nodoc: + def as_json(options = nil) self end #:nodoc: + def encode_json(encoder) to_s end #:nodoc: end class NilClass - AS_JSON = ActiveSupport::JSON::Variable.new('null').freeze - def as_json(options = nil) AS_JSON end #:nodoc: + def as_json(options = nil) self end #:nodoc: + def encode_json(encoder) 'null' end #:nodoc: end class String @@ -192,8 +192,8 @@ end class Float # Encoding Infinity or NaN to JSON should return "null". The default returns - # "Infinity" or "NaN" what breaks parsing the JSON. E.g. JSON.parse('[NaN]'). - def as_json(options = nil) finite? ? self : NilClass::AS_JSON end #:nodoc: + # "Infinity" or "NaN" breaks parsing the JSON. E.g. JSON.parse('[NaN]'). + def as_json(options = nil) finite? ? self : nil end #:nodoc: end class BigDecimal @@ -211,7 +211,7 @@ class BigDecimal if finite? ActiveSupport.encode_big_decimal_as_string ? to_s : self else - NilClass::AS_JSON + nil end end end diff --git a/activesupport/lib/active_support/locale/en.yml b/activesupport/lib/active_support/locale/en.yml index a1499bcc90..18c7d47026 100644 --- a/activesupport/lib/active_support/locale/en.yml +++ b/activesupport/lib/active_support/locale/en.yml @@ -34,3 +34,102 @@ en: words_connector: ", " two_words_connector: " and " last_word_connector: ", and " + number: + # Used in NumberHelper.number_to_delimited() + # These are also the defaults for 'currency', 'percentage', 'precision', and 'human' + format: + # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5) + separator: "." + # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three) + delimiter: "," + # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00) + precision: 3 + # If set to true, precision will mean the number of significant digits instead + # of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2) + significant: false + # If set, the zeros after the decimal separator will always be stripped (eg.: 1.200 will be 1.2) + strip_insignificant_zeros: false + + # Used in NumberHelper.number_to_currency() + currency: + format: + # Where is the currency sign? %u is the currency unit, %n the number (default: $5.00) + format: "%u%n" + unit: "$" + # These five are to override number.format and are optional + separator: "." + delimiter: "," + precision: 2 + significant: false + strip_insignificant_zeros: false + + # Used in NumberHelper.number_to_percentage() + percentage: + format: + # These five are to override number.format and are optional + # separator: + delimiter: "" + # precision: + # significant: false + # strip_insignificant_zeros: false + format: "%n%" + + # Used in NumberHelper.number_to_rounded() + precision: + format: + # These five are to override number.format and are optional + # separator: + delimiter: "" + # precision: + # significant: false + # strip_insignificant_zeros: false + + # Used in NumberHelper.number_to_human_size() and NumberHelper.number_to_human() + human: + format: + # These five are to override number.format and are optional + # separator: + delimiter: "" + precision: 3 + significant: true + strip_insignificant_zeros: true + # Used in number_to_human_size() + storage_units: + # Storage units output formatting. + # %u is the storage unit, %n is the number (default: 2 MB) + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + # Used in NumberHelper.number_to_human() + decimal_units: + format: "%n %u" + # Decimal units output formatting + # By default we will only quantify some of the exponents + # but the commented ones might be defined or overridden + # by the user. + units: + # femto: Quadrillionth + # pico: Trillionth + # nano: Billionth + # micro: Millionth + # mili: Thousandth + # centi: Hundredth + # deci: Tenth + unit: "" + # ten: + # one: Ten + # other: Tens + # hundred: Hundred + thousand: Thousand + million: Million + billion: Billion + trillion: Trillion + quadrillion: Quadrillion + +
\ No newline at end of file diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 4fe925f7f4..87b1d76026 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -76,7 +76,7 @@ module ActiveSupport #:nodoc: # # 'Café périferôl'.mb_chars.split(/é/).map { |part| part.upcase.to_s } # => ["CAF", " P", "RIFERÔL"] def split(*args) - @wrapped_string.split(*args).map { |i| i.mb_chars } + @wrapped_string.split(*args).map { |i| self.class.new(i) } end # Works like like <tt>String#slice!</tt>, but returns an instance of Chars, or nil if the string was not diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb new file mode 100644 index 0000000000..fc97782697 --- /dev/null +++ b/activesupport/lib/active_support/number_helper.rb @@ -0,0 +1,531 @@ +require 'active_support/core_ext/big_decimal/conversions' +require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/hash/keys' +require 'active_support/i18n' + +module ActiveSupport + module NumberHelper + extend self + + DEFAULT_CURRENCY_VALUES = { :format => "%u%n", :negative_format => "-%u%n", :unit => "$", :separator => ".", :delimiter => ",", + :precision => 2, :significant => false, :strip_insignificant_zeros => false } + + # Formats a +number+ into a US phone number (e.g., (555) + # 123-9876). You can customize the format in the +options+ hash. + # + # ==== Options + # + # * <tt>:area_code</tt> - Adds parentheses around the area code. + # * <tt>:delimiter</tt> - Specifies the delimiter to use + # (defaults to "-"). + # * <tt>:extension</tt> - Specifies an extension to add to the + # end of the generated number. + # * <tt>:country_code</tt> - Sets the country code for the phone + # number. + # ==== Examples + # + # number_to_phone(5551234) # => 555-1234 + # number_to_phone("5551234") # => 555-1234 + # number_to_phone(1235551234) # => 123-555-1234 + # number_to_phone(1235551234, :area_code => true) # => (123) 555-1234 + # number_to_phone(1235551234, :delimiter => " ") # => 123 555 1234 + # number_to_phone(1235551234, :area_code => true, :extension => 555) # => (123) 555-1234 x 555 + # number_to_phone(1235551234, :country_code => 1) # => +1-123-555-1234 + # number_to_phone("123a456") # => 123a456 + # + # number_to_phone(1235551234, :country_code => 1, :extension => 1343, :delimiter => ".") + # # => +1.123.555.1234 x 1343 + def number_to_phone(number, options = {}) + return unless number + options = options.symbolize_keys + + number = number.to_s.strip + area_code = options[:area_code] + delimiter = options[:delimiter] || "-" + extension = options[:extension] + country_code = options[:country_code] + + if area_code + number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3") + else + number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") + number.slice!(0, 1) if number.start_with?(delimiter) && !delimiter.blank? + end + + str = '' + str << "+#{country_code}#{delimiter}" unless country_code.blank? + str << number + str << " x #{extension}" unless extension.blank? + str + end + + # Formats a +number+ into a currency string (e.g., $13.65). You + # can customize the format in the +options+ hash. + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:precision</tt> - Sets the level of precision (defaults + # to 2). + # * <tt>:unit</tt> - Sets the denomination of the currency + # (defaults to "$"). + # * <tt>:separator</tt> - Sets the separator between the units + # (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ","). + # * <tt>:format</tt> - Sets the format for non-negative numbers + # (defaults to "%u%n"). Fields are <tt>%u</tt> for the + # currency, and <tt>%n</tt> for the number. + # * <tt>:negative_format</tt> - Sets the format for negative + # numbers (defaults to prepending an hyphen to the formatted + # number given by <tt>:format</tt>). Accepts the same fields + # than <tt>:format</tt>, except <tt>%n</tt> is here the + # absolute value of the number. + # + # ==== Examples + # + # number_to_currency(1234567890.50) # => $1,234,567,890.50 + # number_to_currency(1234567890.506) # => $1,234,567,890.51 + # number_to_currency(1234567890.506, :precision => 3) # => $1,234,567,890.506 + # number_to_currency(1234567890.506, :locale => :fr) # => 1 234 567 890,51 € + # number_to_currency("123a456") # => $123a456 + # + # number_to_currency(-1234567890.50, :negative_format => "(%u%n)") + # # => ($1,234,567,890.50) + # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "") + # # => £1234567890,50 + # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "", :format => "%n %u") + # # => 1234567890,50 £ + def number_to_currency(number, options = {}) + return unless number + options = options.symbolize_keys + + currency = translations_for('currency', options[:locale]) + currency[:negative_format] ||= "-" + currency[:format] if currency[:format] + + defaults = DEFAULT_CURRENCY_VALUES.merge(defaults_translations(options[:locale])).merge!(currency) + defaults[:negative_format] = "-" + options[:format] if options[:format] + options = defaults.merge!(options) + + unit = options.delete(:unit) + format = options.delete(:format) + + if number.to_f.phase != 0 + format = options.delete(:negative_format) + number = number.respond_to?("abs") ? number.abs : number.sub(/^-/, '') + end + + formatted_number = format.gsub('%n', self.number_to_rounded(number, options)).gsub('%u', unit) + formatted_number + end + + # Formats a +number+ as a percentage string (e.g., 65%). You can + # customize the format in the +options+ hash. + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:precision</tt> - Sets the precision of the number + # (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # + # of significant_digits. If +false+, the # of fractional + # digits (defaults to +false+). + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +false+). + # * <tt>:format</tt> - Specifies the format of the percentage + # string The number field is <tt>%n</tt> (defaults to "%n%"). + # + # ==== Examples + # + # number_to_percentage(100) # => 100.000% + # number_to_percentage("98") # => 98.000% + # number_to_percentage(100, :precision => 0) # => 100% + # number_to_percentage(1000, :delimiter => '.', :separator => ',') # => 1.000,000% + # number_to_percentage(302.24398923423, :precision => 5) # => 302.24399% + # number_to_percentage(1000, :locale => :fr) # => 1 000,000% + # number_to_percentage("98a") # => 98a% + # number_to_percentage(100, :format => "%n %") # => 100 % + # + def number_to_percentage(number, options = {}) + return unless number + options = options.symbolize_keys + + defaults = format_translations('percentage', options[:locale]) + options = defaults.merge!(options) + + format = options[:format] || "%n%" + + formatted_number = format.gsub('%n', self.number_to_rounded(number, options)) + formatted_number + end + + # Formats a +number+ with grouped thousands using +delimiter+ + # (e.g., 12,324). You can customize the format in the +options+ + # hash. + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ","). + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # + # ==== Examples + # + # number_to_delimited(12345678) # => 12,345,678 + # number_to_delimited("123456") # => 123,456 + # number_to_delimited(12345678.05) # => 12,345,678.05 + # number_to_delimited(12345678, :delimiter => ".") # => 12.345.678 + # number_to_delimited(12345678, :delimiter => ",") # => 12,345,678 + # number_to_delimited(12345678.05, :separator => " ") # => 12,345,678 05 + # number_to_delimited(12345678.05, :locale => :fr) # => 12 345 678,05 + # number_to_delimited("112a") # => 112a + # number_to_delimited(98765432.98, :delimiter => " ", :separator => ",") + # # => 98 765 432,98 + def number_to_delimited(number, options = {}) + options = options.symbolize_keys + + return number unless valid_float?(number) + + options = defaults_translations(options[:locale]).merge(options) + + parts = number.to_s.to_str.split('.') + parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}") + parts.join(options[:separator]) + end + + # Formats a +number+ with the specified level of + # <tt>:precision</tt> (e.g., 112.32 has a precision of 2 if + # +:significant+ is +false+, and 5 if +:significant+ is +true+). + # You can customize the format in the +options+ hash. + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:precision</tt> - Sets the precision of the number + # (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # + # of significant_digits. If +false+, the # of fractional + # digits (defaults to +false+). + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +false+). + # + # ==== Examples + # + # number_to_rounded(111.2345) # => 111.235 + # number_to_rounded(111.2345, :precision => 2) # => 111.23 + # number_to_rounded(13, :precision => 5) # => 13.00000 + # number_to_rounded(389.32314, :precision => 0) # => 389 + # number_to_rounded(111.2345, :significant => true) # => 111 + # number_to_rounded(111.2345, :precision => 1, :significant => true) # => 100 + # number_to_rounded(13, :precision => 5, :significant => true) # => 13.000 + # number_to_rounded(111.234, :locale => :fr) # => 111,234 + # + # number_to_rounded(13, :precision => 5, :significant => true, :strip_insignificant_zeros => true) + # # => 13 + # + # number_to_rounded(389.32314, :precision => 4, :significant => true) # => 389.3 + # number_to_rounded(1111.2345, :precision => 2, :separator => ',', :delimiter => '.') + # # => 1.111,23 + def number_to_rounded(number, options = {}) + options = options.symbolize_keys + + return number unless valid_float?(number) + number = Float(number) + + defaults = format_translations('precision', options[:locale]) + options = defaults.merge!(options) + + precision = options.delete :precision + significant = options.delete :significant + strip_insignificant_zeros = options.delete :strip_insignificant_zeros + + if significant and precision > 0 + if number == 0 + digits, rounded_number = 1, 0 + else + digits = (Math.log10(number.abs) + 1).floor + rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new((10 ** (digits - precision)).to_f.to_s)).round.to_f * 10 ** (digits - precision) + digits = (Math.log10(rounded_number.abs) + 1).floor # After rounding, the number of digits may have changed + end + precision -= digits + precision = precision > 0 ? precision : 0 #don't let it be negative + else + rounded_number = BigDecimal.new(number.to_s).round(precision).to_f + rounded_number = rounded_number.zero? ? rounded_number.abs : rounded_number #prevent showing negative zeros + end + formatted_number = self.number_to_delimited("%01.#{precision}f" % rounded_number, options) + if strip_insignificant_zeros + escaped_separator = Regexp.escape(options[:separator]) + formatted_number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '') + else + formatted_number + end + end + + STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb].freeze + + # Formats the bytes in +number+ into a more understandable + # representation (e.g., giving it 1500 yields 1.5 KB). This + # method is useful for reporting file sizes to users. You can + # customize the format in the +options+ hash. + # + # See <tt>number_to_human</tt> if you want to pretty-print a + # generic number. + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:precision</tt> - Sets the precision of the number + # (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # + # of significant_digits. If +false+, the # of fractional + # digits (defaults to +true+) + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +true+) + # * <tt>:prefix</tt> - If +:si+ formats the number using the SI + # prefix (defaults to :binary) + # + # ==== Examples + # + # number_to_human_size(123) # => 123 Bytes + # number_to_human_size(1234) # => 1.21 KB + # number_to_human_size(12345) # => 12.1 KB + # number_to_human_size(1234567) # => 1.18 MB + # number_to_human_size(1234567890) # => 1.15 GB + # number_to_human_size(1234567890123) # => 1.12 TB + # number_to_human_size(1234567, :precision => 2) # => 1.2 MB + # number_to_human_size(483989, :precision => 2) # => 470 KB + # number_to_human_size(1234567, :precision => 2, :separator => ',') # => 1,2 MB + # + # Non-significant zeros after the fractional separator are + # stripped out by default (set + # <tt>:strip_insignificant_zeros</tt> to +false+ to change that): + # number_to_human_size(1234567890123, :precision => 5) # => "1.1229 TB" + # number_to_human_size(524288000, :precision => 5) # => "500 MB" + def number_to_human_size(number, options = {}) + options = options.symbolize_keys + + return number unless valid_float?(number) + number = Float(number) + + defaults = format_translations('human', options[:locale]) + options = defaults.merge!(options) + + #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files + options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) + + storage_units_format = I18n.translate(:'number.human.storage_units.format', :locale => options[:locale], :raise => true) + + base = options[:prefix] == :si ? 1000 : 1024 + + if number.to_i < base + unit = I18n.translate(:'number.human.storage_units.units.byte', :locale => options[:locale], :count => number.to_i, :raise => true) + storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit) + else + max_exp = STORAGE_UNITS.size - 1 + exponent = (Math.log(number) / Math.log(base)).to_i # Convert to base + exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit + number /= base ** exponent + + unit_key = STORAGE_UNITS[exponent] + unit = I18n.translate(:"number.human.storage_units.units.#{unit_key}", :locale => options[:locale], :count => number, :raise => true) + + formatted_number = self.number_to_rounded(number, options) + storage_units_format.gsub(/%n/, formatted_number).gsub(/%u/, unit) + end + end + + DECIMAL_UNITS = {0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion, + -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto}.freeze + + # Pretty prints (formats and approximates) a number in a way it + # is more readable by humans (eg.: 1200000000 becomes "1.2 + # Billion"). This is useful for numbers that can get very large + # (and too hard to read). + # + # See <tt>number_to_human_size</tt> if you want to print a file + # size. + # + # You can also define you own unit-quantifier names if you want + # to use other decimal units (eg.: 1500 becomes "1.5 + # kilometers", 0.150 becomes "150 milliliters", etc). You may + # define a wide range of unit quantifiers, even fractional ones + # (centi, deci, mili, etc). + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:precision</tt> - Sets the precision of the number + # (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # + # of significant_digits. If +false+, the # of fractional + # digits (defaults to +true+) + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +true+) + # * <tt>:units</tt> - A Hash of unit quantifier names. Or a + # string containing an i18n scope where to find this hash. It + # might have the following keys: + # * *integers*: <tt>:unit</tt>, <tt>:ten</tt>, + # *<tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>, + # *<tt>:billion</tt>, <tt>:trillion</tt>, + # *<tt>:quadrillion</tt> + # * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>, + # *<tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>, + # *<tt>:pico</tt>, <tt>:femto</tt> + # * <tt>:format</tt> - Sets the format of the output string + # (defaults to "%n %u"). The field types are: + # * %u - The quantifier (ex.: 'thousand') + # * %n - The number + # + # ==== Examples + # + # number_to_human(123) # => "123" + # number_to_human(1234) # => "1.23 Thousand" + # number_to_human(12345) # => "12.3 Thousand" + # number_to_human(1234567) # => "1.23 Million" + # number_to_human(1234567890) # => "1.23 Billion" + # number_to_human(1234567890123) # => "1.23 Trillion" + # number_to_human(1234567890123456) # => "1.23 Quadrillion" + # number_to_human(1234567890123456789) # => "1230 Quadrillion" + # number_to_human(489939, :precision => 2) # => "490 Thousand" + # number_to_human(489939, :precision => 4) # => "489.9 Thousand" + # number_to_human(1234567, :precision => 4, + # :significant => false) # => "1.2346 Million" + # number_to_human(1234567, :precision => 1, + # :separator => ',', + # :significant => false) # => "1,2 Million" + # + # Non-significant zeros after the decimal separator are stripped + # out by default (set <tt>:strip_insignificant_zeros</tt> to + # +false+ to change that): + # number_to_human(12345012345, :significant_digits => 6) # => "12.345 Billion" + # number_to_human(500000000, :precision => 5) # => "500 Million" + # + # ==== Custom Unit Quantifiers + # + # You can also use your own custom unit quantifiers: + # number_to_human(500000, :units => {:unit => "ml", :thousand => "lt"}) # => "500 lt" + # + # If in your I18n locale you have: + # distance: + # centi: + # one: "centimeter" + # other: "centimeters" + # unit: + # one: "meter" + # other: "meters" + # thousand: + # one: "kilometer" + # other: "kilometers" + # billion: "gazillion-distance" + # + # Then you could do: + # + # number_to_human(543934, :units => :distance) # => "544 kilometers" + # number_to_human(54393498, :units => :distance) # => "54400 kilometers" + # number_to_human(54393498000, :units => :distance) # => "54.4 gazillion-distance" + # number_to_human(343, :units => :distance, :precision => 1) # => "300 meters" + # number_to_human(1, :units => :distance) # => "1 meter" + # number_to_human(0.34, :units => :distance) # => "34 centimeters" + def number_to_human(number, options = {}) + options = options.symbolize_keys + + return number unless valid_float?(number) + number = Float(number) + + defaults = format_translations('human', options[:locale]) + options = defaults.merge!(options) + + #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files + options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) + + inverted_du = DECIMAL_UNITS.invert + + units = options.delete :units + unit_exponents = case units + when Hash + units + when String, Symbol + I18n.translate(:"#{units}", :locale => options[:locale], :raise => true) + when nil + I18n.translate(:"number.human.decimal_units.units", :locale => options[:locale], :raise => true) + else + raise ArgumentError, ":units must be a Hash or String translation scope." + end.keys.map{|e_name| inverted_du[e_name] }.sort_by{|e| -e} + + number_exponent = number != 0 ? Math.log10(number.abs).floor : 0 + display_exponent = unit_exponents.find{ |e| number_exponent >= e } || 0 + number /= 10 ** display_exponent + + unit = case units + when Hash + units[DECIMAL_UNITS[display_exponent]] + when String, Symbol + I18n.translate(:"#{units}.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i) + else + I18n.translate(:"number.human.decimal_units.units.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i) + end + + decimal_format = options[:format] || I18n.translate(:'number.human.decimal_units.format', :locale => options[:locale], :default => "%n %u") + formatted_number = self.number_to_rounded(number, options) + decimal_format.gsub(/%n/, formatted_number).gsub(/%u/, unit).strip + end + + def self.private_module_and_instance_method(method_name) + private method_name + private_class_method method_name + end + private_class_method :private_module_and_instance_method + + def format_translations(namespace, locale) + defaults_translations(locale).merge(translations_for(namespace, locale)) + end + private_module_and_instance_method :format_translations + + def defaults_translations(locale) + I18n.translate(:'number.format', :locale => locale, :default => {}) + end + private_module_and_instance_method :defaults_translations + + def translations_for(namespace, locale) + I18n.translate(:"number.#{namespace}.format", :locale => locale, :default => {}) + end + private_module_and_instance_method :translations_for + + def valid_float?(number) + Float(number) + rescue ArgumentError, TypeError + false + end + private_module_and_instance_method :valid_float? + + end +end diff --git a/activesupport/lib/active_support/testing/performance.rb b/activesupport/lib/active_support/testing/performance.rb index 2bea0f991a..517926c74d 100644 --- a/activesupport/lib/active_support/testing/performance.rb +++ b/activesupport/lib/active_support/testing/performance.rb @@ -3,7 +3,8 @@ require 'rails/version' require 'active_support/concern' require 'active_support/core_ext/class/delegating_attributes' require 'active_support/core_ext/string/inflections' -require 'action_view/helpers/number_helper' +require 'active_support/core_ext/module/delegation' +require 'active_support/number_helper' module ActiveSupport module Testing @@ -195,8 +196,7 @@ module ActiveSupport end class Base - include ActionView::Helpers::NumberHelper - include ActionView::Helpers::OutputSafetyHelper + include ActiveSupport::NumberHelper attr_reader :total @@ -240,7 +240,7 @@ module ActiveSupport class Amount < Base def format(measurement) - number_with_delimiter(measurement.floor) + number_to_delimited(measurement.floor) end end diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb index 772c7b4209..527fa555b7 100644 --- a/activesupport/lib/active_support/testing/setup_and_teardown.rb +++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb @@ -4,6 +4,14 @@ require 'active_support/callbacks' module ActiveSupport module Testing module SetupAndTeardown + + PASSTHROUGH_EXCEPTIONS = [ + NoMemoryError, + SignalException, + Interrupt, + SystemExit + ] + extend ActiveSupport::Concern included do @@ -28,11 +36,15 @@ module ActiveSupport run_callbacks :setup do result = super end + rescue *PASSTHROUGH_EXCEPTIONS + raise rescue Exception => e result = runner.puke(self.class, method_name, e) ensure begin run_callbacks :teardown + rescue *PASSTHROUGH_EXCEPTIONS + raise rescue Exception => e result = runner.puke(self.class, method_name, e) end diff --git a/activesupport/test/core_ext/bigdecimal_test.rb b/activesupport/test/core_ext/bigdecimal_test.rb index e24a089650..a5987044b9 100644 --- a/activesupport/test/core_ext/bigdecimal_test.rb +++ b/activesupport/test/core_ext/bigdecimal_test.rb @@ -14,4 +14,9 @@ class BigDecimalTest < ActiveSupport::TestCase bd = BigDecimal.new '10' assert_equal bd, bd.to_d end + + def test_to_s + bd = BigDecimal.new '0.01' + assert_equal '0.01', bd.to_s + end end diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index 6e1b3ca010..bd41311739 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -234,7 +234,7 @@ class ModuleTest < ActiveSupport::TestCase def test_local_constant_names ActiveSupport::Deprecation.silence do - assert_equal %w(Constant1 Constant3), Ab.local_constant_names + assert_equal %w(Constant1 Constant3), Ab.local_constant_names.sort.map(&:to_s) end end end diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index 1cb1e25d4c..435f4aa5a1 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -186,3 +186,264 @@ class NumericExtSizeTest < ActiveSupport::TestCase assert_equal 3458764513820540928, 3.exabyte end end + +class NumericExtFormattingTest < ActiveSupport::TestCase + def kilobytes(number) + number * 1024 + end + + def megabytes(number) + kilobytes(number) * 1024 + end + + def gigabytes(number) + megabytes(number) * 1024 + end + + def terabytes(number) + gigabytes(number) * 1024 + end + + def test_to_s__phone + assert_equal("555-1234", 5551234.to_s(:phone)) + assert_equal("800-555-1212", 8005551212.to_s(:phone)) + assert_equal("(800) 555-1212", 8005551212.to_s(:phone, :area_code => true)) + assert_equal("800 555 1212", 8005551212.to_s(:phone, :delimiter => " ")) + assert_equal("(800) 555-1212 x 123", 8005551212.to_s(:phone, :area_code => true, :extension => 123)) + assert_equal("800-555-1212", 8005551212.to_s(:phone, :extension => " ")) + assert_equal("555.1212", 5551212.to_s(:phone, :delimiter => '.')) + assert_equal("+1-800-555-1212", 8005551212.to_s(:phone, :country_code => 1)) + assert_equal("+18005551212", 8005551212.to_s(:phone, :country_code => 1, :delimiter => '')) + assert_equal("22-555-1212", 225551212.to_s(:phone)) + assert_equal("+45-22-555-1212", 225551212.to_s(:phone, :country_code => 45)) + end + + def test_to_s__currency + assert_equal("$1,234,567,890.50", 1234567890.50.to_s(:currency)) + assert_equal("$1,234,567,890.51", 1234567890.506.to_s(:currency)) + assert_equal("-$1,234,567,890.50", -1234567890.50.to_s(:currency)) + assert_equal("-$ 1,234,567,890.50", -1234567890.50.to_s(:currency, :format => "%u %n")) + assert_equal("($1,234,567,890.50)", -1234567890.50.to_s(:currency, :negative_format => "(%u%n)")) + assert_equal("$1,234,567,892", 1234567891.50.to_s(:currency, :precision => 0)) + assert_equal("$1,234,567,890.5", 1234567890.50.to_s(:currency, :precision => 1)) + assert_equal("£1234567890,50", 1234567890.50.to_s(:currency, :unit => "£", :separator => ",", :delimiter => "")) + end + + + def test_to_s__rounded + assert_equal("-111.235", -111.2346.to_s(:rounded)) + assert_equal("111.235", 111.2346.to_s(:rounded)) + assert_equal("31.83", 31.825.to_s(:rounded, :precision => 2)) + assert_equal("111.23", 111.2346.to_s(:rounded, :precision => 2)) + assert_equal("111.00", 111.to_s(:rounded, :precision => 2)) + assert_equal("3268", (32.6751 * 100.00).to_s(:rounded, :precision => 0)) + assert_equal("112", 111.50.to_s(:rounded, :precision => 0)) + assert_equal("1234567892", 1234567891.50.to_s(:rounded, :precision => 0)) + assert_equal("0", 0.to_s(:rounded, :precision => 0)) + assert_equal("0.00100", 0.001.to_s(:rounded, :precision => 5)) + assert_equal("0.001", 0.00111.to_s(:rounded, :precision => 3)) + assert_equal("10.00", 9.995.to_s(:rounded, :precision => 2)) + assert_equal("11.00", 10.995.to_s(:rounded, :precision => 2)) + assert_equal("0.00", -0.001.to_s(:rounded, :precision => 2)) + end + + def test_to_s__percentage + assert_equal("100.000%", 100.to_s(:percentage)) + assert_equal("100%", 100.to_s(:percentage, :precision => 0)) + assert_equal("302.06%", 302.0574.to_s(:percentage, :precision => 2)) + assert_equal("123.4%", 123.400.to_s(:percentage, :precision => 3, :strip_insignificant_zeros => true)) + assert_equal("1.000,000%", 1000.to_s(:percentage, :delimiter => '.', :separator => ',')) + assert_equal("1000.000 %", 1000.to_s(:percentage, :format => "%n %")) + end + + def test_to_s__delimited + assert_equal("12,345,678", 12345678.to_s(:delimited)) + assert_equal("0", 0.to_s(:delimited)) + assert_equal("123", 123.to_s(:delimited)) + assert_equal("123,456", 123456.to_s(:delimited)) + assert_equal("123,456.78", 123456.78.to_s(:delimited)) + assert_equal("123,456.789", 123456.789.to_s(:delimited)) + assert_equal("123,456.78901", 123456.78901.to_s(:delimited)) + assert_equal("123,456,789.78901", 123456789.78901.to_s(:delimited)) + assert_equal("0.78901", 0.78901.to_s(:delimited)) + end + + def test_to_s__delimited__with_options_hash + assert_equal '12 345 678', 12345678.to_s(:delimited, :delimiter => ' ') + assert_equal '12,345,678-05', 12345678.05.to_s(:delimited, :separator => '-') + assert_equal '12.345.678,05', 12345678.05.to_s(:delimited, :separator => ',', :delimiter => '.') + assert_equal '12.345.678,05', 12345678.05.to_s(:delimited, :delimiter => '.', :separator => ',') + end + + + def test_to_s__rounded_with_custom_delimiter_and_separator + assert_equal '31,83', 31.825.to_s(:rounded, :precision => 2, :separator => ',') + assert_equal '1.231,83', 1231.825.to_s(:rounded, :precision => 2, :separator => ',', :delimiter => '.') + end + + def test_to_s__rounded__with_significant_digits + assert_equal "124000", 123987.to_s(:rounded, :precision => 3, :significant => true) + assert_equal "120000000", 123987876.to_s(:rounded, :precision => 2, :significant => true ) + assert_equal "9775", 9775.to_s(:rounded, :precision => 4, :significant => true ) + assert_equal "5.4", 5.3923.to_s(:rounded, :precision => 2, :significant => true ) + assert_equal "5", 5.3923.to_s(:rounded, :precision => 1, :significant => true ) + assert_equal "1", 1.232.to_s(:rounded, :precision => 1, :significant => true ) + assert_equal "7", 7.to_s(:rounded, :precision => 1, :significant => true ) + assert_equal "1", 1.to_s(:rounded, :precision => 1, :significant => true ) + assert_equal "53", 52.7923.to_s(:rounded, :precision => 2, :significant => true ) + assert_equal "9775.00", 9775.to_s(:rounded, :precision => 6, :significant => true ) + assert_equal "5.392900", 5.3929.to_s(:rounded, :precision => 7, :significant => true ) + assert_equal "0.0", 0.to_s(:rounded, :precision => 2, :significant => true ) + assert_equal "0", 0.to_s(:rounded, :precision => 1, :significant => true ) + assert_equal "0.0001", 0.0001.to_s(:rounded, :precision => 1, :significant => true ) + assert_equal "0.000100", 0.0001.to_s(:rounded, :precision => 3, :significant => true ) + assert_equal "0.0001", 0.0001111.to_s(:rounded, :precision => 1, :significant => true ) + assert_equal "10.0", 9.995.to_s(:rounded, :precision => 3, :significant => true) + assert_equal "9.99", 9.994.to_s(:rounded, :precision => 3, :significant => true) + assert_equal "11.0", 10.995.to_s(:rounded, :precision => 3, :significant => true) + end + + def test_to_s__rounded__with_strip_insignificant_zeros + assert_equal "9775.43", 9775.43.to_s(:rounded, :precision => 4, :strip_insignificant_zeros => true ) + assert_equal "9775.2", 9775.2.to_s(:rounded, :precision => 6, :significant => true, :strip_insignificant_zeros => true ) + assert_equal "0", 0.to_s(:rounded, :precision => 6, :significant => true, :strip_insignificant_zeros => true ) + end + + def test_to_s__rounded__with_significant_true_and_zero_precision + # Zero precision with significant is a mistake (would always return zero), + # so we treat it as if significant was false (increases backwards compatibility for number_to_human_size) + assert_equal "124", 123.987.to_s(:rounded, :precision => 0, :significant => true) + assert_equal "12", 12.to_s(:rounded, :precision => 0, :significant => true ) + end + + def test_to_s__human_size + assert_equal '0 Bytes', 0.to_s(:human_size) + assert_equal '1 Byte', 1.to_s(:human_size) + assert_equal '3 Bytes', 3.14159265.to_s(:human_size) + assert_equal '123 Bytes', 123.0.to_s(:human_size) + assert_equal '123 Bytes', 123.to_s(:human_size) + assert_equal '1.21 KB', 1234.to_s(:human_size) + assert_equal '12.1 KB', 12345.to_s(:human_size) + assert_equal '1.18 MB', 1234567.to_s(:human_size) + assert_equal '1.15 GB', 1234567890.to_s(:human_size) + assert_equal '1.12 TB', 1234567890123.to_s(:human_size) + assert_equal '1030 TB', terabytes(1026).to_s(:human_size) + assert_equal '444 KB', kilobytes(444).to_s(:human_size) + assert_equal '1020 MB', megabytes(1023).to_s(:human_size) + assert_equal '3 TB', terabytes(3).to_s(:human_size) + assert_equal '1.2 MB', 1234567.to_s(:human_size, :precision => 2) + assert_equal '3 Bytes', 3.14159265.to_s(:human_size, :precision => 4) + assert_equal '1 KB', kilobytes(1.0123).to_s(:human_size, :precision => 2) + assert_equal '1.01 KB', kilobytes(1.0100).to_s(:human_size, :precision => 4) + assert_equal '10 KB', kilobytes(10.000).to_s(:human_size, :precision => 4) + assert_equal '1 Byte', 1.1.to_s(:human_size) + assert_equal '10 Bytes', 10.to_s(:human_size) + end + + def test_to_s__human_size_with_si_prefix + assert_equal '3 Bytes', 3.14159265.to_s(:human_size, :prefix => :si) + assert_equal '123 Bytes', 123.0.to_s(:human_size, :prefix => :si) + assert_equal '123 Bytes', 123.to_s(:human_size, :prefix => :si) + assert_equal '1.23 KB', 1234.to_s(:human_size, :prefix => :si) + assert_equal '12.3 KB', 12345.to_s(:human_size, :prefix => :si) + assert_equal '1.23 MB', 1234567.to_s(:human_size, :prefix => :si) + assert_equal '1.23 GB', 1234567890.to_s(:human_size, :prefix => :si) + assert_equal '1.23 TB', 1234567890123.to_s(:human_size, :prefix => :si) + end + + def test_to_s__human_size_with_options_hash + assert_equal '1.2 MB', 1234567.to_s(:human_size, :precision => 2) + assert_equal '3 Bytes', 3.14159265.to_s(:human_size, :precision => 4) + assert_equal '1 KB', kilobytes(1.0123).to_s(:human_size, :precision => 2) + assert_equal '1.01 KB', kilobytes(1.0100).to_s(:human_size, :precision => 4) + assert_equal '10 KB', kilobytes(10.000).to_s(:human_size, :precision => 4) + assert_equal '1 TB', 1234567890123.to_s(:human_size, :precision => 1) + assert_equal '500 MB', 524288000.to_s(:human_size, :precision=>3) + assert_equal '10 MB', 9961472.to_s(:human_size, :precision=>0) + assert_equal '40 KB', 41010.to_s(:human_size, :precision => 1) + assert_equal '40 KB', 41100.to_s(:human_size, :precision => 2) + assert_equal '1.0 KB', kilobytes(1.0123).to_s(:human_size, :precision => 2, :strip_insignificant_zeros => false) + assert_equal '1.012 KB', kilobytes(1.0123).to_s(:human_size, :precision => 3, :significant => false) + assert_equal '1 KB', kilobytes(1.0123).to_s(:human_size, :precision => 0, :significant => true) #ignores significant it precision is 0 + end + + def test_to_s__human_size_with_custom_delimiter_and_separator + assert_equal '1,01 KB', kilobytes(1.0123).to_s(:human_size, :precision => 3, :separator => ',') + assert_equal '1,01 KB', kilobytes(1.0100).to_s(:human_size, :precision => 4, :separator => ',') + assert_equal '1.000,1 TB', terabytes(1000.1).to_s(:human_size, :precision => 5, :delimiter => '.', :separator => ',') + end + + def test_number_to_human + assert_equal '-123', -123.to_s(:human) + assert_equal '-0.5', -0.5.to_s(:human) + assert_equal '0', 0.to_s(:human) + assert_equal '0.5', 0.5.to_s(:human) + assert_equal '123', 123.to_s(:human) + assert_equal '1.23 Thousand', 1234.to_s(:human) + assert_equal '12.3 Thousand', 12345.to_s(:human) + assert_equal '1.23 Million', 1234567.to_s(:human) + assert_equal '1.23 Billion', 1234567890.to_s(:human) + assert_equal '1.23 Trillion', 1234567890123.to_s(:human) + assert_equal '1.23 Quadrillion', 1234567890123456.to_s(:human) + assert_equal '1230 Quadrillion', 1234567890123456789.to_s(:human) + assert_equal '490 Thousand', 489939.to_s(:human, :precision => 2) + assert_equal '489.9 Thousand', 489939.to_s(:human, :precision => 4) + assert_equal '489 Thousand', 489000.to_s(:human, :precision => 4) + assert_equal '489.0 Thousand', 489000.to_s(:human, :precision => 4, :strip_insignificant_zeros => false) + assert_equal '1.2346 Million', 1234567.to_s(:human, :precision => 4, :significant => false) + assert_equal '1,2 Million', 1234567.to_s(:human, :precision => 1, :significant => false, :separator => ',') + assert_equal '1 Million', 1234567.to_s(:human, :precision => 0, :significant => true, :separator => ',') #significant forced to false + end + + def test_number_to_human_with_custom_units + #Only integers + volume = {:unit => "ml", :thousand => "lt", :million => "m3"} + assert_equal '123 lt', 123456.to_s(:human, :units => volume) + assert_equal '12 ml', 12.to_s(:human, :units => volume) + assert_equal '1.23 m3', 1234567.to_s(:human, :units => volume) + + #Including fractionals + distance = {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"} + assert_equal '1.23 mm', 0.00123.to_s(:human, :units => distance) + assert_equal '1.23 cm', 0.0123.to_s(:human, :units => distance) + assert_equal '1.23 dm', 0.123.to_s(:human, :units => distance) + assert_equal '1.23 m', 1.23.to_s(:human, :units => distance) + assert_equal '1.23 dam', 12.3.to_s(:human, :units => distance) + assert_equal '1.23 hm', 123.to_s(:human, :units => distance) + assert_equal '1.23 km', 1230.to_s(:human, :units => distance) + assert_equal '1.23 km', 1230.to_s(:human, :units => distance) + assert_equal '1.23 km', 1230.to_s(:human, :units => distance) + assert_equal '12.3 km', 12300.to_s(:human, :units => distance) + + #The quantifiers don't need to be a continuous sequence + gangster = {:hundred => "hundred bucks", :million => "thousand quids"} + assert_equal '1 hundred bucks', 100.to_s(:human, :units => gangster) + assert_equal '25 hundred bucks', 2500.to_s(:human, :units => gangster) + assert_equal '25 thousand quids', 25000000.to_s(:human, :units => gangster) + assert_equal '12300 thousand quids', 12345000000.to_s(:human, :units => gangster) + + #Spaces are stripped from the resulting string + assert_equal '4', 4.to_s(:human, :units => {:unit => "", :ten => 'tens '}) + assert_equal '4.5 tens', 45.to_s(:human, :units => {:unit => "", :ten => ' tens '}) + end + + def test_number_to_human_with_custom_format + assert_equal '123 times Thousand', 123456.to_s(:human, :format => "%n times %u") + volume = {:unit => "ml", :thousand => "lt", :million => "m3"} + assert_equal '123.lt', 123456.to_s(:human, :units => volume, :format => "%n.%u") + end + + def test_to_s__injected_on_proper_types + assert_equal Fixnum, 1230.class + assert_equal '1.23 Thousand', 1230.to_s(:human) + + assert_equal Float, Float(1230).class + assert_equal '1.23 Thousand', Float(1230).to_s(:human) + + assert_equal Bignum, (100**10).class + assert_equal '100000 Quadrillion', (100**10).to_s(:human) + + assert_equal BigDecimal, BigDecimal("1000010").class + assert_equal '1 Million', BigDecimal("1000010").to_s(:human) + end +end diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index 0566ebf291..212ee262a3 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -285,6 +285,12 @@ class TestJSONEncoding < ActiveSupport::TestCase end end + def test_nil_true_and_false_represented_as_themselves + assert_equal nil, nil.as_json + assert_equal true, true.as_json + assert_equal false, false.as_json + end + protected def object_keys(json_object) diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb new file mode 100644 index 0000000000..9b7d7f020c --- /dev/null +++ b/activesupport/test/number_helper_test.rb @@ -0,0 +1,375 @@ +require 'abstract_unit' +require 'active_support/number_helper' + +module ActiveSupport + module NumberHelper + class NumberHelperTest < ActiveSupport::TestCase + + class TestClassWithInstanceNumberHelpers + include ActiveSupport::NumberHelper + end + + class TestClassWithClassNumberHelpers + extend ActiveSupport::NumberHelper + end + + def setup + @instance_with_helpers = TestClassWithInstanceNumberHelpers.new + end + + def kilobytes(number) + number * 1024 + end + + def megabytes(number) + kilobytes(number) * 1024 + end + + def gigabytes(number) + megabytes(number) * 1024 + end + + def terabytes(number) + gigabytes(number) * 1024 + end + + def test_number_to_phone + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal("555-1234", number_helper.number_to_phone(5551234)) + assert_equal("800-555-1212", number_helper.number_to_phone(8005551212)) + assert_equal("(800) 555-1212", number_helper.number_to_phone(8005551212, {:area_code => true})) + assert_equal("", number_helper.number_to_phone("", {:area_code => true})) + assert_equal("800 555 1212", number_helper.number_to_phone(8005551212, {:delimiter => " "})) + assert_equal("(800) 555-1212 x 123", number_helper.number_to_phone(8005551212, {:area_code => true, :extension => 123})) + assert_equal("800-555-1212", number_helper.number_to_phone(8005551212, :extension => " ")) + assert_equal("555.1212", number_helper.number_to_phone(5551212, :delimiter => '.')) + assert_equal("800-555-1212", number_helper.number_to_phone("8005551212")) + assert_equal("+1-800-555-1212", number_helper.number_to_phone(8005551212, :country_code => 1)) + assert_equal("+18005551212", number_helper.number_to_phone(8005551212, :country_code => 1, :delimiter => '')) + assert_equal("22-555-1212", number_helper.number_to_phone(225551212)) + assert_equal("+45-22-555-1212", number_helper.number_to_phone(225551212, :country_code => 45)) + end + end + + def test_number_to_currency + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal("$1,234,567,890.50", number_helper.number_to_currency(1234567890.50)) + assert_equal("$1,234,567,890.51", number_helper.number_to_currency(1234567890.506)) + assert_equal("-$1,234,567,890.50", number_helper.number_to_currency(-1234567890.50)) + assert_equal("-$ 1,234,567,890.50", number_helper.number_to_currency(-1234567890.50, {:format => "%u %n"})) + assert_equal("($1,234,567,890.50)", number_helper.number_to_currency(-1234567890.50, {:negative_format => "(%u%n)"})) + assert_equal("$1,234,567,892", number_helper.number_to_currency(1234567891.50, {:precision => 0})) + assert_equal("$1,234,567,890.5", number_helper.number_to_currency(1234567890.50, {:precision => 1})) + assert_equal("£1234567890,50", number_helper.number_to_currency(1234567890.50, {:unit => "£", :separator => ",", :delimiter => ""})) + assert_equal("$1,234,567,890.50", number_helper.number_to_currency("1234567890.50")) + assert_equal("1,234,567,890.50 Kč", number_helper.number_to_currency("1234567890.50", {:unit => "Kč", :format => "%n %u"})) + assert_equal("1,234,567,890.50 - Kč", number_helper.number_to_currency("-1234567890.50", {:unit => "Kč", :format => "%n %u", :negative_format => "%n - %u"})) + assert_equal("0.00", number_helper.number_to_currency(+0.0, {:unit => "", :negative_format => "(%n)"})) + assert_equal("(0.00)", number_helper.number_to_currency(-0.0, {:unit => "", :negative_format => "(%n)"})) + end + end + + def test_number_to_percentage + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal("100.000%", number_helper.number_to_percentage(100)) + assert_equal("100%", number_helper.number_to_percentage(100, {:precision => 0})) + assert_equal("302.06%", number_helper.number_to_percentage(302.0574, {:precision => 2})) + assert_equal("100.000%", number_helper.number_to_percentage("100")) + assert_equal("1000.000%", number_helper.number_to_percentage("1000")) + assert_equal("123.4%", number_helper.number_to_percentage(123.400, :precision => 3, :strip_insignificant_zeros => true)) + assert_equal("1.000,000%", number_helper.number_to_percentage(1000, :delimiter => '.', :separator => ',')) + assert_equal("1000.000 %", number_helper.number_to_percentage(1000, :format => "%n %")) + end + end + + def test_to_delimited + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal("12,345,678", number_helper.number_to_delimited(12345678)) + assert_equal("0", number_helper.number_to_delimited(0)) + assert_equal("123", number_helper.number_to_delimited(123)) + assert_equal("123,456", number_helper.number_to_delimited(123456)) + assert_equal("123,456.78", number_helper.number_to_delimited(123456.78)) + assert_equal("123,456.789", number_helper.number_to_delimited(123456.789)) + assert_equal("123,456.78901", number_helper.number_to_delimited(123456.78901)) + assert_equal("123,456,789.78901", number_helper.number_to_delimited(123456789.78901)) + assert_equal("0.78901", number_helper.number_to_delimited(0.78901)) + assert_equal("123,456.78", number_helper.number_to_delimited("123456.78")) + end + end + + def test_to_delimited_with_options_hash + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '12 345 678', number_helper.number_to_delimited(12345678, :delimiter => ' ') + assert_equal '12,345,678-05', number_helper.number_to_delimited(12345678.05, :separator => '-') + assert_equal '12.345.678,05', number_helper.number_to_delimited(12345678.05, :separator => ',', :delimiter => '.') + assert_equal '12.345.678,05', number_helper.number_to_delimited(12345678.05, :delimiter => '.', :separator => ',') + end + end + + def test_to_rounded + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal("-111.235", number_helper.number_to_rounded(-111.2346)) + assert_equal("111.235", number_helper.number_to_rounded(111.2346)) + assert_equal("31.83", number_helper.number_to_rounded(31.825, :precision => 2)) + assert_equal("111.23", number_helper.number_to_rounded(111.2346, :precision => 2)) + assert_equal("111.00", number_helper.number_to_rounded(111, :precision => 2)) + assert_equal("111.235", number_helper.number_to_rounded("111.2346")) + assert_equal("31.83", number_helper.number_to_rounded("31.825", :precision => 2)) + assert_equal("3268", number_helper.number_to_rounded((32.6751 * 100.00), :precision => 0)) + assert_equal("112", number_helper.number_to_rounded(111.50, :precision => 0)) + assert_equal("1234567892", number_helper.number_to_rounded(1234567891.50, :precision => 0)) + assert_equal("0", number_helper.number_to_rounded(0, :precision => 0)) + assert_equal("0.00100", number_helper.number_to_rounded(0.001, :precision => 5)) + assert_equal("0.001", number_helper.number_to_rounded(0.00111, :precision => 3)) + assert_equal("10.00", number_helper.number_to_rounded(9.995, :precision => 2)) + assert_equal("11.00", number_helper.number_to_rounded(10.995, :precision => 2)) + assert_equal("0.00", number_helper.number_to_rounded(-0.001, :precision => 2)) + end + end + + def test_to_rounded_with_custom_delimiter_and_separator + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '31,83', number_helper.number_to_rounded(31.825, :precision => 2, :separator => ',') + assert_equal '1.231,83', number_helper.number_to_rounded(1231.825, :precision => 2, :separator => ',', :delimiter => '.') + end + end + + def test_to_rounded_with_significant_digits + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal "124000", number_helper.number_to_rounded(123987, :precision => 3, :significant => true) + assert_equal "120000000", number_helper.number_to_rounded(123987876, :precision => 2, :significant => true ) + assert_equal "40000", number_helper.number_to_rounded("43523", :precision => 1, :significant => true ) + assert_equal "9775", number_helper.number_to_rounded(9775, :precision => 4, :significant => true ) + assert_equal "5.4", number_helper.number_to_rounded(5.3923, :precision => 2, :significant => true ) + assert_equal "5", number_helper.number_to_rounded(5.3923, :precision => 1, :significant => true ) + assert_equal "1", number_helper.number_to_rounded(1.232, :precision => 1, :significant => true ) + assert_equal "7", number_helper.number_to_rounded(7, :precision => 1, :significant => true ) + assert_equal "1", number_helper.number_to_rounded(1, :precision => 1, :significant => true ) + assert_equal "53", number_helper.number_to_rounded(52.7923, :precision => 2, :significant => true ) + assert_equal "9775.00", number_helper.number_to_rounded(9775, :precision => 6, :significant => true ) + assert_equal "5.392900", number_helper.number_to_rounded(5.3929, :precision => 7, :significant => true ) + assert_equal "0.0", number_helper.number_to_rounded(0, :precision => 2, :significant => true ) + assert_equal "0", number_helper.number_to_rounded(0, :precision => 1, :significant => true ) + assert_equal "0.0001", number_helper.number_to_rounded(0.0001, :precision => 1, :significant => true ) + assert_equal "0.000100", number_helper.number_to_rounded(0.0001, :precision => 3, :significant => true ) + assert_equal "0.0001", number_helper.number_to_rounded(0.0001111, :precision => 1, :significant => true ) + assert_equal "10.0", number_helper.number_to_rounded(9.995, :precision => 3, :significant => true) + assert_equal "9.99", number_helper.number_to_rounded(9.994, :precision => 3, :significant => true) + assert_equal "11.0", number_helper.number_to_rounded(10.995, :precision => 3, :significant => true) + end + end + + def test_to_rounded_with_strip_insignificant_zeros + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal "9775.43", number_helper.number_to_rounded(9775.43, :precision => 4, :strip_insignificant_zeros => true ) + assert_equal "9775.2", number_helper.number_to_rounded(9775.2, :precision => 6, :significant => true, :strip_insignificant_zeros => true ) + assert_equal "0", number_helper.number_to_rounded(0, :precision => 6, :significant => true, :strip_insignificant_zeros => true ) + end + end + + def test_to_rounded_with_significant_true_and_zero_precision + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + # Zero precision with significant is a mistake (would always return zero), + # so we treat it as if significant was false (increases backwards compatibility for number_to_human_size) + assert_equal "124", number_helper.number_to_rounded(123.987, :precision => 0, :significant => true) + assert_equal "12", number_helper.number_to_rounded(12, :precision => 0, :significant => true ) + assert_equal "12", number_helper.number_to_rounded("12.3", :precision => 0, :significant => true ) + end + end + + def test_number_number_to_human_size + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '0 Bytes', number_helper.number_to_human_size(0) + assert_equal '1 Byte', number_helper.number_to_human_size(1) + assert_equal '3 Bytes', number_helper.number_to_human_size(3.14159265) + assert_equal '123 Bytes', number_helper.number_to_human_size(123.0) + assert_equal '123 Bytes', number_helper.number_to_human_size(123) + assert_equal '1.21 KB', number_helper.number_to_human_size(1234) + assert_equal '12.1 KB', number_helper.number_to_human_size(12345) + assert_equal '1.18 MB', number_helper.number_to_human_size(1234567) + assert_equal '1.15 GB', number_helper.number_to_human_size(1234567890) + assert_equal '1.12 TB', number_helper.number_to_human_size(1234567890123) + assert_equal '1030 TB', number_helper.number_to_human_size(terabytes(1026)) + assert_equal '444 KB', number_helper.number_to_human_size(kilobytes(444)) + assert_equal '1020 MB', number_helper.number_to_human_size(megabytes(1023)) + assert_equal '3 TB', number_helper.number_to_human_size(terabytes(3)) + assert_equal '1.2 MB', number_helper.number_to_human_size(1234567, :precision => 2) + assert_equal '3 Bytes', number_helper.number_to_human_size(3.14159265, :precision => 4) + assert_equal '123 Bytes', number_helper.number_to_human_size('123') + assert_equal '1 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 2) + assert_equal '1.01 KB', number_helper.number_to_human_size(kilobytes(1.0100), :precision => 4) + assert_equal '10 KB', number_helper.number_to_human_size(kilobytes(10.000), :precision => 4) + assert_equal '1 Byte', number_helper.number_to_human_size(1.1) + assert_equal '10 Bytes', number_helper.number_to_human_size(10) + end + end + + def test_number_to_human_size_with_si_prefix + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '3 Bytes', number_helper.number_to_human_size(3.14159265, :prefix => :si) + assert_equal '123 Bytes', number_helper.number_to_human_size(123.0, :prefix => :si) + assert_equal '123 Bytes', number_helper.number_to_human_size(123, :prefix => :si) + assert_equal '1.23 KB', number_helper.number_to_human_size(1234, :prefix => :si) + assert_equal '12.3 KB', number_helper.number_to_human_size(12345, :prefix => :si) + assert_equal '1.23 MB', number_helper.number_to_human_size(1234567, :prefix => :si) + assert_equal '1.23 GB', number_helper.number_to_human_size(1234567890, :prefix => :si) + assert_equal '1.23 TB', number_helper.number_to_human_size(1234567890123, :prefix => :si) + end + end + + def test_number_to_human_size_with_options_hash + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '1.2 MB', number_helper.number_to_human_size(1234567, :precision => 2) + assert_equal '3 Bytes', number_helper.number_to_human_size(3.14159265, :precision => 4) + assert_equal '1 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 2) + assert_equal '1.01 KB', number_helper.number_to_human_size(kilobytes(1.0100), :precision => 4) + assert_equal '10 KB', number_helper.number_to_human_size(kilobytes(10.000), :precision => 4) + assert_equal '1 TB', number_helper.number_to_human_size(1234567890123, :precision => 1) + assert_equal '500 MB', number_helper.number_to_human_size(524288000, :precision=>3) + assert_equal '10 MB', number_helper.number_to_human_size(9961472, :precision=>0) + assert_equal '40 KB', number_helper.number_to_human_size(41010, :precision => 1) + assert_equal '40 KB', number_helper.number_to_human_size(41100, :precision => 2) + assert_equal '1.0 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 2, :strip_insignificant_zeros => false) + assert_equal '1.012 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 3, :significant => false) + assert_equal '1 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 0, :significant => true) #ignores significant it precision is 0 + end + end + + def test_number_to_human_size_with_custom_delimiter_and_separator + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '1,01 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 3, :separator => ',') + assert_equal '1,01 KB', number_helper.number_to_human_size(kilobytes(1.0100), :precision => 4, :separator => ',') + assert_equal '1.000,1 TB', number_helper.number_to_human_size(terabytes(1000.1), :precision => 5, :delimiter => '.', :separator => ',') + end + end + + def test_number_to_human + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '-123', number_helper.number_to_human(-123) + assert_equal '-0.5', number_helper.number_to_human(-0.5) + assert_equal '0', number_helper.number_to_human(0) + assert_equal '0.5', number_helper.number_to_human(0.5) + assert_equal '123', number_helper.number_to_human(123) + assert_equal '1.23 Thousand', number_helper.number_to_human(1234) + assert_equal '12.3 Thousand', number_helper.number_to_human(12345) + assert_equal '1.23 Million', number_helper.number_to_human(1234567) + assert_equal '1.23 Billion', number_helper.number_to_human(1234567890) + assert_equal '1.23 Trillion', number_helper.number_to_human(1234567890123) + assert_equal '1.23 Quadrillion', number_helper.number_to_human(1234567890123456) + assert_equal '1230 Quadrillion', number_helper.number_to_human(1234567890123456789) + assert_equal '490 Thousand', number_helper.number_to_human(489939, :precision => 2) + assert_equal '489.9 Thousand', number_helper.number_to_human(489939, :precision => 4) + assert_equal '489 Thousand', number_helper.number_to_human(489000, :precision => 4) + assert_equal '489.0 Thousand', number_helper.number_to_human(489000, :precision => 4, :strip_insignificant_zeros => false) + assert_equal '1.2346 Million', number_helper.number_to_human(1234567, :precision => 4, :significant => false) + assert_equal '1,2 Million', number_helper.number_to_human(1234567, :precision => 1, :significant => false, :separator => ',') + assert_equal '1 Million', number_helper.number_to_human(1234567, :precision => 0, :significant => true, :separator => ',') #significant forced to false + end + end + + def test_number_to_human_with_custom_units + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + #Only integers + volume = {:unit => "ml", :thousand => "lt", :million => "m3"} + assert_equal '123 lt', number_helper.number_to_human(123456, :units => volume) + assert_equal '12 ml', number_helper.number_to_human(12, :units => volume) + assert_equal '1.23 m3', number_helper.number_to_human(1234567, :units => volume) + + #Including fractionals + distance = {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"} + assert_equal '1.23 mm', number_helper.number_to_human(0.00123, :units => distance) + assert_equal '1.23 cm', number_helper.number_to_human(0.0123, :units => distance) + assert_equal '1.23 dm', number_helper.number_to_human(0.123, :units => distance) + assert_equal '1.23 m', number_helper.number_to_human(1.23, :units => distance) + assert_equal '1.23 dam', number_helper.number_to_human(12.3, :units => distance) + assert_equal '1.23 hm', number_helper.number_to_human(123, :units => distance) + assert_equal '1.23 km', number_helper.number_to_human(1230, :units => distance) + assert_equal '1.23 km', number_helper.number_to_human(1230, :units => distance) + assert_equal '1.23 km', number_helper.number_to_human(1230, :units => distance) + assert_equal '12.3 km', number_helper.number_to_human(12300, :units => distance) + + #The quantifiers don't need to be a continuous sequence + gangster = {:hundred => "hundred bucks", :million => "thousand quids"} + assert_equal '1 hundred bucks', number_helper.number_to_human(100, :units => gangster) + assert_equal '25 hundred bucks', number_helper.number_to_human(2500, :units => gangster) + assert_equal '25 thousand quids', number_helper.number_to_human(25000000, :units => gangster) + assert_equal '12300 thousand quids', number_helper.number_to_human(12345000000, :units => gangster) + + #Spaces are stripped from the resulting string + assert_equal '4', number_helper.number_to_human(4, :units => {:unit => "", :ten => 'tens '}) + assert_equal '4.5 tens', number_helper.number_to_human(45, :units => {:unit => "", :ten => ' tens '}) + end + end + + def test_number_to_human_with_custom_format + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '123 times Thousand', number_helper.number_to_human(123456, :format => "%n times %u") + volume = {:unit => "ml", :thousand => "lt", :million => "m3"} + assert_equal '123.lt', number_helper.number_to_human(123456, :units => volume, :format => "%n.%u") + end + end + + def test_number_helpers_should_return_nil_when_given_nil + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_nil number_helper.number_to_phone(nil) + assert_nil number_helper.number_to_currency(nil) + assert_nil number_helper.number_to_percentage(nil) + assert_nil number_helper.number_to_delimited(nil) + assert_nil number_helper.number_to_rounded(nil) + assert_nil number_helper.number_to_human_size(nil) + assert_nil number_helper.number_to_human(nil) + end + end + + def test_number_helpers_do_not_mutate_options_hash + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + options = { 'raise' => true } + + number_helper.number_to_phone(1, options) + assert_equal({ 'raise' => true }, options) + + number_helper.number_to_currency(1, options) + assert_equal({ 'raise' => true }, options) + + number_helper.number_to_percentage(1, options) + assert_equal({ 'raise' => true }, options) + + number_helper.number_to_delimited(1, options) + assert_equal({ 'raise' => true }, options) + + number_helper.number_to_rounded(1, options) + assert_equal({ 'raise' => true }, options) + + number_helper.number_to_human_size(1, options) + assert_equal({ 'raise' => true }, options) + + number_helper.number_to_human(1, options) + assert_equal({ 'raise' => true }, options) + end + end + + def test_number_helpers_should_return_non_numeric_param_unchanged + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal("+1-x x 123", number_helper.number_to_phone("x", :country_code => 1, :extension => 123)) + assert_equal("x", number_helper.number_to_phone("x")) + assert_equal("$x.", number_helper.number_to_currency("x.")) + assert_equal("$x", number_helper.number_to_currency("x")) + assert_equal("x%", number_helper.number_to_percentage("x")) + assert_equal("x", number_helper.number_to_delimited("x")) + assert_equal("x.", number_helper.number_to_rounded("x.")) + assert_equal("x", number_helper.number_to_rounded("x")) + assert_equal "x", number_helper.number_to_human_size('x') + assert_equal "x", number_helper.number_to_human('x') + end + end + + def test_extending_or_including_number_helper_correctly_hides_private_methods + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert !number_helper.respond_to?(:valid_float?) + assert number_helper.respond_to?(:valid_float?, true) + end + end + + end + end +end diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb index e5b5547478..c02bfa8497 100644 --- a/activesupport/test/test_case_test.rb +++ b/activesupport/test/test_case_test.rb @@ -18,11 +18,9 @@ module ActiveSupport end end - def test_callback_with_exception + def test_standard_error_raised_within_setup_callback_is_puked tc = Class.new(TestCase) do - def self.name - nil - end + def self.name; nil; end setup :bad_callback def bad_callback; raise 'oh noes' end @@ -41,11 +39,9 @@ module ActiveSupport assert_equal 'oh noes', exception.message end - def test_teardown_callback_with_exception + def test_standard_error_raised_within_teardown_callback_is_puked tc = Class.new(TestCase) do - def self.name - nil - end + def self.name; nil; end teardown :bad_callback def bad_callback; raise 'oh noes' end @@ -63,5 +59,51 @@ module ActiveSupport assert_equal test_name, name assert_equal 'oh noes', exception.message end + + def test_passthrough_exception_raised_within_test_method_is_not_rescued + tc = Class.new(TestCase) do + def self.name; nil; end + + def test_which_raises_interrupt; raise Interrupt; end + end + + test_name = 'test_which_raises_interrupt' + fr = FakeRunner.new + + test = tc.new test_name + assert_raises(Interrupt) { test.run fr } + end + + def test_passthrough_exception_raised_within_setup_callback_is_not_rescued + tc = Class.new(TestCase) do + def self.name; nil; end + + setup :callback_which_raises_interrupt + def callback_which_raises_interrupt; raise Interrupt; end + def test_true; assert true end + end + + test_name = 'test_true' + fr = FakeRunner.new + + test = tc.new test_name + assert_raises(Interrupt) { test.run fr } + end + + def test_passthrough_exception_raised_within_teardown_callback_is_not_rescued + tc = Class.new(TestCase) do + def self.name; nil; end + + teardown :callback_which_raises_interrupt + def callback_which_raises_interrupt; raise Interrupt; end + def test_true; assert true end + end + + test_name = 'test_true' + fr = FakeRunner.new + + test = tc.new test_name + assert_raises(Interrupt) { test.run fr } + end end end diff --git a/activesupport/test/testing/performance_test.rb b/activesupport/test/testing/performance_test.rb new file mode 100644 index 0000000000..74d7dae9e7 --- /dev/null +++ b/activesupport/test/testing/performance_test.rb @@ -0,0 +1,40 @@ +require 'abstract_unit' +require 'active_support/testing/performance' + + +module ActiveSupport + module Testing + class PerformanceTest < ActiveSupport::TestCase + def test_amount_format + amount_metric = ActiveSupport::Testing::Performance::Metrics[:amount].new + assert_equal "0", amount_metric.format(0) + assert_equal "1", amount_metric.format(1.23) + assert_equal "40,000,000", amount_metric.format(40000000) + end + + def test_time_format + time_metric = ActiveSupport::Testing::Performance::Metrics[:time].new + assert_equal "0 ms", time_metric.format(0) + assert_equal "40 ms", time_metric.format(0.04) + assert_equal "41 ms", time_metric.format(0.0415) + assert_equal "1.23 sec", time_metric.format(1.23) + assert_equal "40000.00 sec", time_metric.format(40000) + assert_equal "-5000 ms", time_metric.format(-5) + end + + def test_space_format + space_metric = ActiveSupport::Testing::Performance::Metrics[:digital_information_unit].new + assert_equal "0 Bytes", space_metric.format(0) + assert_equal "0 Bytes", space_metric.format(0.4) + assert_equal "1 Byte", space_metric.format(1.23) + assert_equal "123 Bytes", space_metric.format(123) + assert_equal "123 Bytes", space_metric.format(123.45) + assert_equal "12 KB", space_metric.format(12345) + assert_equal "1.2 MB", space_metric.format(1234567) + assert_equal "9.3 GB", space_metric.format(10**10) + assert_equal "91 TB", space_metric.format(10**14) + assert_equal "910000 TB", space_metric.format(10**18) + end + end + end +end
\ No newline at end of file diff --git a/guides/Rakefile b/guides/Rakefile index ad4ff91fe6..d005a12936 100644 --- a/guides/Rakefile +++ b/guides/Rakefile @@ -1,11 +1,71 @@ -desc 'Generate guides (for authors), use ONLY=foo to process just "foo.textile"' -task :generate_guides do - ENV["WARN_BROKEN_LINKS"] = "1" # authors can't disable this - ruby "rails_guides.rb" -end +namespace :guides do + + desc 'Generate guides (for authors), use ONLY=foo to process just "foo.textile"' + task :generate => 'generate:html' + + namespace :generate do + + desc "Generate HTML guides" + task :html do + ENV["WARN_BROKEN_LINKS"] = "1" # authors can't disable this + ruby "rails_guides.rb" + end + + desc "Generate .mobi file. The kindlegen executable must be in your PATH. You can get it for free from http://www.amazon.com/kindlepublishing" + task :kindle do + ENV['KINDLE'] = '1' + Rake::Task['guides:generate:html'].invoke + end + end + + # Validate guides ------------------------------------------------------------------------- + desc 'Validate guides, use ONLY=foo to process just "foo.html"' + task :validate do + ruby "w3c_validator.rb" + end + + desc "Show help" + task :help do + puts <<-help + +Guides are taken from the source directory, and the resulting HTML goes into the +output directory. Assets are stored under files, and copied to output/files as +part of the generation process. + +All this process is handled via rake tasks, here's a full list of them: -# Validate guides ------------------------------------------------------------------------- -desc 'Validate guides, use ONLY=foo to process just "foo.html"' -task :validate_guides do - ruby "w3c_validator.rb" +#{%x[rake -T]} +Some arguments may be passed via environment variables: + + WARNINGS=1 + Internal links (anchors) are checked, also detects duplicated IDs. + + ALL=1 + Force generation of all guides. + + ONLY=name + Useful if you want to generate only one or a set of guides. + + Generate only association_basics.html: + ONLY=assoc + + Separate many using commas: + ONLY=assoc,migrations + + GUIDES_LANGUAGE + Use it when you want to generate translated guides in + source/<GUIDES_LANGUAGE> folder (such as source/es) + + EDGE=1 + Indicate generated guides should be marked as edge. + +Examples: + $ rake guides:generate ALL=1 + $ rake guides:generate EDGE=1 + $ rake guides:generate:kindle EDGE=1 + $ rake guides:generate GUIDES_LANGUAGE=es + help + end end + +task :default => 'guides:help' diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb index d6a98f9ac4..230bebf3bb 100644 --- a/guides/rails_guides/generator.rb +++ b/guides/rails_guides/generator.rb @@ -1,7 +1,7 @@ # --------------------------------------------------------------------------- # -# This script generates the guides. It can be invoked either directly or via the -# generate_guides rake task within the railties directory. +# This script generates the guides. It can be invoked via the +# guides:generate rake task within the guides directory. # # Guides are taken from the source directory, and the resulting HTML goes into the # output directory. Assets are stored under files, and copied to output/files as @@ -47,11 +47,6 @@ # Set to "1" to indicate generated guides should be marked as edge. This # inserts a badge and changes the preamble of the home page. # -# KINDLE -# Set to "1" to generate the .mobi with all the guides. The kindlegen -# executable must be in your PATH. You can get it for free from -# http://www.amazon.com/kindlepublishing -# # --------------------------------------------------------------------------- require 'set' diff --git a/guides/source/active_support_core_extensions.textile b/guides/source/active_support_core_extensions.textile index 587f65529e..2addc50d68 100644 --- a/guides/source/active_support_core_extensions.textile +++ b/guides/source/active_support_core_extensions.textile @@ -1840,6 +1840,76 @@ date and time arithmetic. NOTE: Defined in +active_support/core_ext/numeric/time.rb+. +h4. Formatting + +Enables the formatting of numbers in a variety of ways. + +Produce a string representation of a number as a telephone number: +<ruby> +5551234.to_s(:phone) # => 555-1234 +1235551234.to_s(:phone) # => 123-555-1234 +1235551234.to_s(:phone, :area_code => true) # => (123) 555-1234 +1235551234.to_s(:phone, :delimiter => " ") # => 123 555 1234 +1235551234.to_s(:phone, :area_code => true, :extension => 555) # => (123) 555-1234 x 555 +1235551234.to_s(:phone, :country_code => 1) # => +1-123-555-1234 +</ruby> + +Produce a string representation of a number as currency: +<ruby> +1234567890.50.to_s(:currency) # => $1,234,567,890.50 +1234567890.506.to_s(:currency) # => $1,234,567,890.51 +1234567890.506.to_s(:currency, :precision => 3) # => $1,234,567,890.506 +</ruby> + +Produce a string representation of a number as a percentage: +<ruby> +100.to_s(:percentage) # => 100.000% +100.to_s(:percentage, :precision => 0) # => 100% +1000.to_s(:percentage, :delimiter => '.', :separator => ',') # => 1.000,000% +302.24398923423.to_s(:percentage, :precision => 5) # => 302.24399% +</ruby> + +Produce a string representation of a number in delimited form: +<ruby> +12345678.to_s(:delimited) # => 12,345,678 +12345678.05.to_s(:delimited) # => 12,345,678.05 +12345678.to_s(:delimited, :delimiter => ".") # => 12.345.678 +12345678.to_s(:delimited, :delimiter => ",") # => 12,345,678 +12345678.05.to_s(:delimited, :separator => " ") # => 12,345,678 05 +</ruby> + +Produce a string representation of a number rounded to a precision: +<ruby> +111.2345.to_s(:rounded) # => 111.235 +111.2345.to_s(:rounded, :precision => 2) # => 111.23 +13.to_s(:rounded, :precision => 5) # => 13.00000 +389.32314.to_s(:rounded, :precision => 0) # => 389 +111.2345.to_s(:rounded, :significant => true) # => 111 +</ruby> + +Produce a string representation of a number as a human-readable number of bytes: +<ruby> +123.to_s(:human_size) # => 123 Bytes +1234.to_s(:human_size) # => 1.21 KB +12345.to_s(:human_size) # => 12.1 KB +1234567.to_s(:human_size) # => 1.18 MB +1234567890.to_s(:human_size) # => 1.15 GB +1234567890123.to_s(:human_size) # => 1.12 TB +</ruby> + +Produce a string representation of a number in human-readable words: +<ruby> +123.to_s(:human) # => "123" +1234.to_s(:human) # => "1.23 Thousand" +12345.to_s(:human) # => "12.3 Thousand" +1234567.to_s(:human) # => "1.23 Million" +1234567890.to_s(:human) # => "1.23 Billion" +1234567890123.to_s(:human) # => "1.23 Trillion" +1234567890123456.to_s(:human) # => "1.23 Quadrillion" +</ruby> + +NOTE: Defined in +active_support/core_ext/numeric/formatting.rb+. + h3. Extensions to +Integer+ h4. +multiple_of?+ diff --git a/guides/source/ruby_on_rails_guides_guidelines.textile b/guides/source/ruby_on_rails_guides_guidelines.textile index f3e934d38c..dd209b61d6 100644 --- a/guides/source/ruby_on_rails_guides_guidelines.textile +++ b/guides/source/ruby_on_rails_guides_guidelines.textile @@ -47,7 +47,13 @@ h4. Generation To generate all the guides, just +cd+ into the *+guides+* directory and execute: <plain> -bundle exec rake generate_guides +bundle exec rake guides:generate +</plain> + +or + +<plain> +bundle exec rake guides:generate:html </plain> (You may need to run +bundle install+ first to install the required gems.) @@ -56,7 +62,7 @@ To process +my_guide.textile+ and nothing else use the +ONLY+ environment variab <plain> touch my_guide.textile -bundle exec rake generate_guides ONLY=my_guide +bundle exec rake guides:generate ONLY=my_guide </plain> By default, guides that have not been modified are not processed, so +ONLY+ is rarely needed in practice. @@ -68,7 +74,13 @@ It is also recommended that you work with +WARNINGS=1+. This detects duplicate I If you want to generate guides in a language other than English, you can keep them in a separate directory under +source+ (eg. <tt>source/es</tt>) and use the +GUIDES_LANGUAGE+ environment variable: <plain> -bundle exec rake generate_guides GUIDES_LANGUAGE=es +bundle exec rake guides:generate GUIDES_LANGUAGE=es +</plain> + +If you want to see all the environment variables you can use to configure the generation script just run: + +<plain> +rake </plain> h4. Validation @@ -76,7 +88,7 @@ h4. Validation Please validate the generated HTML with: <plain> -bundle exec rake validate_guides +bundle exec rake guides:validate </plain> Particularly, titles get an ID generated from their content and this often leads to duplicates. Please set +WARNINGS=1+ when generating guides to detect them. The warning messages suggest a solution. @@ -85,8 +97,8 @@ h3. Kindle Guides h4(#generation-kindle). Generation -To generate guides for the Kindle, you need to provide +KINDLE=1+ as an environment variable: +To generate guides for the Kindle, use the following rake task: <plain> -KINDLE=1 bundle exec rake generate_guides +bundle exec rake guides:generate:kindle </plain> diff --git a/guides/source/upgrading_ruby_on_rails.textile b/guides/source/upgrading_ruby_on_rails.textile index 02407a5fe8..6cdc6ab289 100644 --- a/guides/source/upgrading_ruby_on_rails.textile +++ b/guides/source/upgrading_ruby_on_rails.textile @@ -34,17 +34,25 @@ h4(#plugins4_0). vendor/plugins Rails 4.0 no longer supports loading plugins from <tt>vendor/plugins</tt>. You must replace any plugins by extracting them to gems and adding them to your Gemfile. If you choose not to make them gems, you can move them into, say, <tt>lib/my_plugin/*</tt> and add an appropriate initializer in <tt>config/initializers/my_plugin.rb</tt>. -h4(#identity_map4_0). IdentityMap +h4(#identity_map4_0). Identity Map -Rails 4.0 has removed <tt>IdentityMap</tt> from <tt>ActiveRecord</tt>, due to "some inconsistencies with associations":https://github.com/rails/rails/commit/302c912bf6bcd0fa200d964ec2dc4a44abe328a6. If you have manually enabled it in your application, you will have to remove the following config that has no effect anymore: <tt>config.active_record.identity_map</tt>. +Rails 4.0 has removed the identity map from Active Record, due to "some inconsistencies with associations":https://github.com/rails/rails/commit/302c912bf6bcd0fa200d964ec2dc4a44abe328a6. If you have manually enabled it in your application, you will have to remove the following config that has no effect anymore: <tt>config.active_record.identity_map</tt>. -h4(#active_model4_0). ActiveModel +h4(#active_record4_0). Active Record -Rails 4.0 has changed how errors attach with the ConfirmationValidator. Now when confirmation validations fail the error will be attached to <tt>:#{attribute}_confirmation</tt> instead of <tt>attribute</tt>. +The <tt>delete</tt> method in collection associations can now receive <tt>Fixnum</tt> or <tt>String</tt> arguments as record ids, besides records, pretty much like the <tt>destroy</tt> method does. Previously it raised <tt>ActiveRecord::AssociationTypeMismatch</tt> for such arguments. From Rails 4.0 on <tt>delete</tt> automatically tries to find the records matching the given ids before deleting them. -h4(#action_pack4_0). ActionPack +h4(#active_model4_0). Active Model -Rails 4.0 changed how <tt>assert_generates</tt>, <tt>assert_recognizes</tt>, and <tt>assert_routing</tt> work. Now all these assertions raise <tt>Assertion</tt> instead of <tt>RoutingError</tt>. +Rails 4.0 has changed how errors attach with the <tt>ActiveModel::Validations::ConfirmationValidator</tt>. Now when confirmation validations fail the error will be attached to <tt>:#{attribute}_confirmation</tt> instead of <tt>attribute</tt>. + +h4(#action_pack4_0). Action Pack + +Rails 4.0 changed how <tt>assert_generates</tt>, <tt>assert_recognizes</tt>, and <tt>assert_routing</tt> work. Now all these assertions raise <tt>Assertion</tt> instead of <tt>ActionController::RoutingError</tt>. + +h4(#helpers_order). Helpers Loading Order + +The loading order of helpers from more than one directory has changed in Rails 4.0. Previously, helpers from all directories were gathered and then sorted alphabetically. After upgrade to Rails 4.0 helpers will preserve the order of loaded directories and will be sorted alphabetically only within each directory. Unless you explicitly use <tt>helpers_path</tt> parameter, this change will only impact the way of loading helpers from engines. If you rely on the fact that particular helper from engine loads before or after another helper from application or another engine, you should check if correct methods are available after upgrade. If you would like to change order in which engines are loaded, you can use <tt>config.railties_order=</tt> method. h3. Upgrading from Rails 3.1 to Rails 3.2 diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 787bafea04..a6abe5ee97 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,5 +1,7 @@ ## Rails 4.0.0 (unreleased) ## +* Add runner to Rails::Railtie as a hook called just after runner starts. *José Valim & kennyj* + * Add `/rails/info/routes` path, displays same information as `rake routes` *Richard Schneeman & Andrew White* * Improved `rake routes` output for redirects *Łukasz Strzałkowski & Andrew White* diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 660e864d2a..32797ee657 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -158,6 +158,14 @@ module Rails self end + # Load the application runner and invoke the registered hooks. + # Check <tt>Rails::Railtie.runner</tt> for more info. + def load_runner(app=self) + initialize_runner + super + self + end + # Stores some of the Rails initial environment parameters which # will be used by middlewares and engines to configure themselves. def env_config @@ -304,6 +312,9 @@ module Rails require "rails/console/helpers" end + def initialize_runner #:nodoc: + end + def build_original_fullpath(env) path_info = env["PATH_INFO"] query_string = env["QUERY_STRING"] diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb index 77f1b15fb4..a672258aa6 100644 --- a/railties/lib/rails/commands/runner.rb +++ b/railties/lib/rails/commands/runner.rb @@ -41,6 +41,7 @@ ENV["RAILS_ENV"] = options[:environment] require APP_PATH Rails.application.require_environment! + Rails.application.load_runner if code_or_file.nil? $stderr.puts "Run '#{$0} -h' for help." diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 4c7199a2e2..806b553b81 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -437,6 +437,11 @@ module Rails super end + def load_runner(app=self) + railties.all { |r| r.load_runner(app) } + super + end + def eager_load! railties.all(&:eager_load!) diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index bf47e66cc4..55a6b3f4f2 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -19,7 +19,7 @@ source 'https://rubygems.org' # gem 'unicorn' # Deploy with Capistrano -# gem 'capistrano', :group => :development +# gem 'capistrano', group: :development # To use debugger # gem 'debugger' diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index 2102f8a03c..c3cc65ab31 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -145,6 +145,12 @@ module Rails @load_console end + def runner(&blk) + @load_runner ||= [] + @load_runner << blk if blk + @load_runner + end + def generators(&blk) @generators ||= [] @generators << blk if blk @@ -179,6 +185,10 @@ module Rails self.class.console.each { |block| block.call(app) } end + def load_runner(app=self) + self.class.runner.each { |block| block.call(app) } + end + def load_tasks(app=self) extend Rake::DSL if defined? Rake::DSL self.class.rake_tasks.each { |block| self.instance_exec(app, &block) } diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 252dd0e31a..d7689863e6 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -288,16 +288,16 @@ module ApplicationTests params = {:authenticity_token => token} get "/posts/1" - assert_match /patch/, last_response.body + assert_match(/patch/, last_response.body) patch "/posts/1", params - assert_match /update/, last_response.body + assert_match(/update/, last_response.body) patch "/posts/1", params assert_equal 200, last_response.status put "/posts/1", params - assert_match /update/, last_response.body + assert_match(/update/, last_response.body) put "/posts/1", params assert_equal 200, last_response.status diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb index 05d73dfc5c..3f4db77897 100644 --- a/railties/test/application/rake/notes_test.rb +++ b/railties/test/application/rake/notes_test.rb @@ -33,14 +33,14 @@ module ApplicationTests output = `bundle exec rake notes` lines = output.scan(/\[([0-9\s]+)\](\s)/) - assert_match /note in erb/, output - assert_match /note in haml/, output - assert_match /note in slim/, output - assert_match /note in ruby/, output - assert_match /note in coffee/, output - assert_match /note in js/, output - assert_match /note in css/, output - assert_match /note in scss/, output + assert_match(/note in erb/, output) + assert_match(/note in haml/, output) + assert_match(/note in slim/, output) + assert_match(/note in ruby/, output) + assert_match(/note in coffee/, output) + assert_match(/note in js/, output) + assert_match(/note in css/, output) + assert_match(/note in scss/, output) assert_equal 8, lines.size @@ -72,12 +72,12 @@ module ApplicationTests output = `bundle exec rake notes` lines = output.scan(/\[([0-9\s]+)\]/).flatten - assert_match /note in app directory/, output - assert_match /note in config directory/, output - assert_match /note in lib directory/, output - assert_match /note in script directory/, output - assert_match /note in test directory/, output - assert_no_match /note in some_other directory/, output + assert_match(/note in app directory/, output) + assert_match(/note in config directory/, output) + assert_match(/note in lib directory/, output) + assert_match(/note in script directory/, output) + assert_match(/note in test directory/, output) + assert_no_match(/note in some_other directory/, output) assert_equal 5, lines.size @@ -108,13 +108,13 @@ module ApplicationTests output = `SOURCE_ANNOTATION_DIRECTORIES='some_other_dir' bundle exec rake notes` lines = output.scan(/\[([0-9\s]+)\]/).flatten - assert_match /note in app directory/, output - assert_match /note in config directory/, output - assert_match /note in lib directory/, output - assert_match /note in script directory/, output - assert_match /note in test directory/, output + assert_match(/note in app directory/, output) + assert_match(/note in config directory/, output) + assert_match(/note in lib directory/, output) + assert_match(/note in script directory/, output) + assert_match(/note in test directory/, output) - assert_match /note in some_other directory/, output + assert_match(/note in some_other directory/, output) assert_equal 6, lines.size diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 27d521485c..8cf867da3c 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -167,5 +167,28 @@ module ApplicationTests end assert !File.exists?(File.join(app_path, 'db', 'schema_cache.dump')) end + + def test_load_activerecord_base_when_we_use_observers + Dir.chdir(app_path) do + `bundle exec rails g model user; + bundle exec rake db:migrate; + bundle exec rails g observer user;` + + add_to_config "config.active_record.observers = :user_observer" + + assert_equal "0", `bundle exec rails r "puts User.count"`.strip + + app_file "lib/tasks/count_user.rake", <<-RUBY + namespace :user do + task :count => :environment do + puts User.count + end + end + RUBY + + assert_equal "0", `bundle exec rake user:count`.strip + end + end + end end diff --git a/railties/test/application/runner_test.rb b/railties/test/application/runner_test.rb index e1d283a7fd..81ed5873a5 100644 --- a/railties/test/application/runner_test.rb +++ b/railties/test/application/runner_test.rb @@ -57,5 +57,15 @@ module ApplicationTests assert_match "script/program_name.rb", Dir.chdir(app_path) { `bundle exec rails runner "script/program_name.rb"` } end + + def test_with_hook + add_to_config <<-RUBY + runner do |app| + app.config.ran = true + end + RUBY + + assert_match "true", Dir.chdir(app_path) { `bundle exec rails runner "puts Rails.application.config.ran"` } + end end end diff --git a/railties/test/commands/console_test.rb b/railties/test/commands/console_test.rb index 9aa1d68675..78648a16b3 100644 --- a/railties/test/commands/console_test.rb +++ b/railties/test/commands/console_test.rb @@ -35,7 +35,7 @@ class Rails::ConsoleTest < ActiveSupport::TestCase start - assert_match /Loading \w+ environment \(Rails/, output + assert_match(/Loading \w+ environment \(Rails/, output) end def test_start_with_debugger @@ -52,7 +52,7 @@ class Rails::ConsoleTest < ActiveSupport::TestCase start ["--sandbox"] - assert_match /Loading \w+ environment in sandbox \(Rails/, output + assert_match(/Loading \w+ environment in sandbox \(Rails/, output) end def test_console_with_environment @@ -61,7 +61,7 @@ class Rails::ConsoleTest < ActiveSupport::TestCase start ["-e production"] - assert_match /production/, output + assert_match(/production/, output) end def test_console_with_rails_environment @@ -70,7 +70,7 @@ class Rails::ConsoleTest < ActiveSupport::TestCase start ["RAILS_ENV=production"] - assert_match /production/, output + assert_match(/production/, output) end diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb index 6d0f5ca073..562b83713b 100644 --- a/railties/test/commands/dbconsole_test.rb +++ b/railties/test/commands/dbconsole_test.rb @@ -12,7 +12,7 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase app_config({}) capture_abort { Rails::DBConsole.config } assert aborted - assert_match /No database is configured for the environment '\w+'/, output + assert_match(/No database is configured for the environment '\w+'/, output) app_config(test: "with_init") assert_equal Rails::DBConsole.config, "with_init" @@ -129,7 +129,7 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase def test_unknown_command_line_client start(adapter: 'unknown', database: 'db') assert aborted - assert_match /Unknown command-line client for db/, output + assert_match(/Unknown command-line client for db/, output) end def test_print_help_short @@ -138,7 +138,7 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase end assert aborted assert_equal '', output - assert_match /Usage:.*dbconsole/, stdout + assert_match(/Usage:.*dbconsole/, stdout) end def test_print_help_long @@ -147,7 +147,7 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase end assert aborted assert_equal '', output - assert_match /Usage:.*dbconsole/, stdout + assert_match(/Usage:.*dbconsole/, stdout) end private diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index 60e7e57a91..417d019178 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -186,7 +186,7 @@ class GeneratorsTest < Rails::Generators::TestCase mkdir_p(File.dirname(template)) File.open(template, 'w'){ |f| f.write "empty" } - output = capture(:stdout) do + capture(:stdout) do Rails::Generators.invoke :model, ["user"], :destination_root => destination_root end @@ -205,7 +205,7 @@ class GeneratorsTest < Rails::Generators::TestCase def test_usage_with_embedded_ruby require File.expand_path("fixtures/lib/generators/usage_template/usage_template_generator", File.dirname(__FILE__)) output = capture(:stdout) { Rails::Generators.invoke :usage_template, ['--help'] } - assert_match /:: 2 ::/, output + assert_match(/:: 2 ::/, output) end def test_hide_namespace diff --git a/railties/test/railties/railtie_test.rb b/railties/test/railties/railtie_test.rb index cd495320b5..c80b0f63af 100644 --- a/railties/test/railties/railtie_test.rb +++ b/railties/test/railties/railtie_test.rb @@ -163,6 +163,22 @@ module RailtiesTest assert $ran_block end + test "runner block is executed when MyApp.load_runner is called" do + $ran_block = false + + class MyTie < Rails::Railtie + runner do + $ran_block = true + end + end + + require "#{app_path}/config/environment" + + assert !$ran_block + AppTemplate::Application.load_runner + assert $ran_block + end + test "railtie can add initializers" do $ran_block = false |