core_ext 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,182 @@
1
+ require 'core_ext/big_decimal/conversions'
2
+ require 'core_ext/object/blank'
3
+ require 'core_ext/hash/keys'
4
+ require 'core_ext/i18n'
5
+ require 'core_ext/class/attribute'
6
+
7
+ module CoreExt
8
+ module NumberHelper
9
+ class NumberConverter # :nodoc:
10
+ # Default and i18n option namespace per class
11
+ class_attribute :namespace
12
+
13
+ # Does the object need a number that is a valid float?
14
+ class_attribute :validate_float
15
+
16
+ attr_reader :number, :opts
17
+
18
+ DEFAULTS = {
19
+ # Used in number_to_delimited
20
+ # These are also the defaults for 'currency', 'percentage', 'precision', and 'human'
21
+ format: {
22
+ # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5)
23
+ separator: ".",
24
+ # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three)
25
+ delimiter: ",",
26
+ # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00)
27
+ precision: 3,
28
+ # If set to true, precision will mean the number of significant digits instead
29
+ # of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2)
30
+ significant: false,
31
+ # If set, the zeros after the decimal separator will always be stripped (eg.: 1.200 will be 1.2)
32
+ strip_insignificant_zeros: false
33
+ },
34
+
35
+ # Used in number_to_currency
36
+ currency: {
37
+ format: {
38
+ format: "%u%n",
39
+ negative_format: "-%u%n",
40
+ unit: "$",
41
+ # These five are to override number.format and are optional
42
+ separator: ".",
43
+ delimiter: ",",
44
+ precision: 2,
45
+ significant: false,
46
+ strip_insignificant_zeros: false
47
+ }
48
+ },
49
+
50
+ # Used in number_to_percentage
51
+ percentage: {
52
+ format: {
53
+ delimiter: "",
54
+ format: "%n%"
55
+ }
56
+ },
57
+
58
+ # Used in number_to_rounded
59
+ precision: {
60
+ format: {
61
+ delimiter: ""
62
+ }
63
+ },
64
+
65
+ # Used in number_to_human_size and number_to_human
66
+ human: {
67
+ format: {
68
+ # These five are to override number.format and are optional
69
+ delimiter: "",
70
+ precision: 3,
71
+ significant: true,
72
+ strip_insignificant_zeros: true
73
+ },
74
+ # Used in number_to_human_size
75
+ storage_units: {
76
+ # Storage units output formatting.
77
+ # %u is the storage unit, %n is the number (default: 2 MB)
78
+ format: "%n %u",
79
+ units: {
80
+ byte: "Bytes",
81
+ kb: "KB",
82
+ mb: "MB",
83
+ gb: "GB",
84
+ tb: "TB"
85
+ }
86
+ },
87
+ # Used in number_to_human
88
+ decimal_units: {
89
+ format: "%n %u",
90
+ # Decimal units output formatting
91
+ # By default we will only quantify some of the exponents
92
+ # but the commented ones might be defined or overridden
93
+ # by the user.
94
+ units: {
95
+ # femto: Quadrillionth
96
+ # pico: Trillionth
97
+ # nano: Billionth
98
+ # micro: Millionth
99
+ # mili: Thousandth
100
+ # centi: Hundredth
101
+ # deci: Tenth
102
+ unit: "",
103
+ # ten:
104
+ # one: Ten
105
+ # other: Tens
106
+ # hundred: Hundred
107
+ thousand: "Thousand",
108
+ million: "Million",
109
+ billion: "Billion",
110
+ trillion: "Trillion",
111
+ quadrillion: "Quadrillion"
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ def self.convert(number, options)
118
+ new(number, options).execute
119
+ end
120
+
121
+ def initialize(number, options)
122
+ @number = number
123
+ @opts = options.symbolize_keys
124
+ end
125
+
126
+ def execute
127
+ if !number
128
+ nil
129
+ elsif validate_float? && !valid_float?
130
+ number
131
+ else
132
+ convert
133
+ end
134
+ end
135
+
136
+ private
137
+
138
+ def options
139
+ @options ||= format_options.merge(opts)
140
+ end
141
+
142
+ def format_options #:nodoc:
143
+ default_format_options.merge!(i18n_format_options)
144
+ end
145
+
146
+ def default_format_options #:nodoc:
147
+ options = DEFAULTS[:format].dup
148
+ options.merge!(DEFAULTS[namespace][:format]) if namespace
149
+ options
150
+ end
151
+
152
+ def i18n_format_options #:nodoc:
153
+ locale = opts[:locale]
154
+ options = I18n.translate(:'number.format', locale: locale, default: {}).dup
155
+
156
+ if namespace
157
+ options.merge!(I18n.translate(:"number.#{namespace}.format", locale: locale, default: {}))
158
+ end
159
+
160
+ options
161
+ end
162
+
163
+ def translate_number_value_with_default(key, i18n_options = {}) #:nodoc:
164
+ I18n.translate(key, { default: default_value(key), scope: :number }.merge!(i18n_options))
165
+ end
166
+
167
+ def translate_in_locale(key, i18n_options = {})
168
+ translate_number_value_with_default(key, { locale: options[:locale] }.merge(i18n_options))
169
+ end
170
+
171
+ def default_value(key)
172
+ key.split('.').reduce(DEFAULTS) { |defaults, k| defaults[k.to_sym] }
173
+ end
174
+
175
+ def valid_float? #:nodoc:
176
+ Float(number)
177
+ rescue ArgumentError, TypeError
178
+ false
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,46 @@
1
+ module CoreExt
2
+ module NumberHelper
3
+ class NumberToCurrencyConverter < NumberConverter # :nodoc:
4
+ self.namespace = :currency
5
+
6
+ def convert
7
+ number = self.number.to_s.strip
8
+ format = options[:format]
9
+
10
+ if is_negative?(number)
11
+ format = options[:negative_format]
12
+ number = absolute_value(number)
13
+ end
14
+
15
+ rounded_number = NumberToRoundedConverter.convert(number, options)
16
+ format.gsub('%n'.freeze, rounded_number).gsub('%u'.freeze, options[:unit])
17
+ end
18
+
19
+ private
20
+
21
+ def is_negative?(number)
22
+ number.to_f.phase != 0
23
+ end
24
+
25
+ def absolute_value(number)
26
+ number.respond_to?(:abs) ? number.abs : number.sub(/\A-/, '')
27
+ end
28
+
29
+ def options
30
+ @options ||= begin
31
+ defaults = default_format_options.merge(i18n_opts)
32
+ # Override negative format if format options are given
33
+ defaults[:negative_format] = "-#{opts[:format]}" if opts[:format]
34
+ defaults.merge!(opts)
35
+ end
36
+ end
37
+
38
+ def i18n_opts
39
+ # Set International negative format if it does not exist
40
+ i18n = i18n_format_options
41
+ i18n[:negative_format] ||= "-#{i18n[:format]}" if i18n[:format]
42
+ i18n
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,28 @@
1
+ module CoreExt
2
+ module NumberHelper
3
+ class NumberToDelimitedConverter < NumberConverter #:nodoc:
4
+ self.validate_float = true
5
+
6
+ DEFAULT_DELIMITER_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/
7
+
8
+ def convert
9
+ parts.join(options[:separator])
10
+ end
11
+
12
+ private
13
+
14
+ def parts
15
+ left, right = number.to_s.split('.')
16
+ left.gsub!(delimiter_pattern) do |digit_to_delimit|
17
+ "#{digit_to_delimit}#{options[:delimiter]}"
18
+ end
19
+ [left, right].compact
20
+ end
21
+
22
+ def delimiter_pattern
23
+ options.fetch(:delimiter_pattern, DEFAULT_DELIMITER_REGEX)
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,68 @@
1
+ module CoreExt
2
+ module NumberHelper
3
+ class NumberToHumanConverter < NumberConverter # :nodoc:
4
+ DECIMAL_UNITS = { 0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion,
5
+ -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto }
6
+ INVERTED_DECIMAL_UNITS = DECIMAL_UNITS.invert
7
+
8
+ self.namespace = :human
9
+ self.validate_float = true
10
+
11
+ def convert # :nodoc:
12
+ @number = Float(number)
13
+
14
+ # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
15
+ unless options.key?(:strip_insignificant_zeros)
16
+ options[:strip_insignificant_zeros] = true
17
+ end
18
+
19
+ units = opts[:units]
20
+ exponent = calculate_exponent(units)
21
+ @number = number / (10 ** exponent)
22
+
23
+ until (rounded_number = NumberToRoundedConverter.convert(number, options)) != NumberToRoundedConverter.convert(1000, options)
24
+ @number = number / 1000.0
25
+ exponent += 3
26
+ end
27
+ unit = determine_unit(units, exponent)
28
+ format.gsub('%n'.freeze, rounded_number).gsub('%u'.freeze, unit).strip
29
+ end
30
+
31
+ private
32
+
33
+ def format
34
+ options[:format] || translate_in_locale('human.decimal_units.format')
35
+ end
36
+
37
+ def determine_unit(units, exponent)
38
+ exp = DECIMAL_UNITS[exponent]
39
+ case units
40
+ when Hash
41
+ units[exp] || ''
42
+ when String, Symbol
43
+ I18n.translate("#{units}.#{exp}", :locale => options[:locale], :count => number.to_i)
44
+ else
45
+ translate_in_locale("human.decimal_units.units.#{exp}", count: number.to_i)
46
+ end
47
+ end
48
+
49
+ def calculate_exponent(units)
50
+ exponent = number != 0 ? Math.log10(number.abs).floor : 0
51
+ unit_exponents(units).find { |e| exponent >= e } || 0
52
+ end
53
+
54
+ def unit_exponents(units)
55
+ case units
56
+ when Hash
57
+ units
58
+ when String, Symbol
59
+ I18n.translate(units.to_s, :locale => options[:locale], :raise => true)
60
+ when nil
61
+ translate_in_locale("human.decimal_units.units", raise: true)
62
+ else
63
+ raise ArgumentError, ":units must be a Hash or String translation scope."
64
+ end.keys.map { |e_name| INVERTED_DECIMAL_UNITS[e_name] }.sort_by(&:-@)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,62 @@
1
+ module CoreExt
2
+ module NumberHelper
3
+ class NumberToHumanSizeConverter < NumberConverter #:nodoc:
4
+ STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb, :pb, :eb]
5
+
6
+ self.namespace = :human
7
+ self.validate_float = true
8
+
9
+ def convert
10
+ if opts.key?(:prefix)
11
+ CoreExt::Deprecation.warn('The :prefix option of `number_to_human_size` is deprecated and will be removed in Rails 5.1 with no replacement.')
12
+ end
13
+
14
+ @number = Float(number)
15
+
16
+ # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
17
+ unless options.key?(:strip_insignificant_zeros)
18
+ options[:strip_insignificant_zeros] = true
19
+ end
20
+
21
+ if smaller_than_base?
22
+ number_to_format = number.to_i.to_s
23
+ else
24
+ human_size = number / (base ** exponent)
25
+ number_to_format = NumberToRoundedConverter.convert(human_size, options)
26
+ end
27
+ conversion_format.gsub('%n'.freeze, number_to_format).gsub('%u'.freeze, unit)
28
+ end
29
+
30
+ private
31
+
32
+ def conversion_format
33
+ translate_number_value_with_default('human.storage_units.format', :locale => options[:locale], :raise => true)
34
+ end
35
+
36
+ def unit
37
+ translate_number_value_with_default(storage_unit_key, :locale => options[:locale], :count => number.to_i, :raise => true)
38
+ end
39
+
40
+ def storage_unit_key
41
+ key_end = smaller_than_base? ? 'byte' : STORAGE_UNITS[exponent]
42
+ "human.storage_units.units.#{key_end}"
43
+ end
44
+
45
+ def exponent
46
+ max = STORAGE_UNITS.size - 1
47
+ exp = (Math.log(number) / Math.log(base)).to_i
48
+ exp = max if exp > max # avoid overflow for the highest unit
49
+ exp
50
+ end
51
+
52
+ def smaller_than_base?
53
+ number.to_i < base
54
+ end
55
+
56
+ def base
57
+ opts[:prefix] == :si ? 1000 : 1024
58
+ end
59
+ end
60
+ end
61
+ end
62
+
@@ -0,0 +1,12 @@
1
+ module CoreExt
2
+ module NumberHelper
3
+ class NumberToPercentageConverter < NumberConverter # :nodoc:
4
+ self.namespace = :percentage
5
+
6
+ def convert
7
+ rounded_number = NumberToRoundedConverter.convert(number, options)
8
+ options[:format].gsub('%n'.freeze, rounded_number)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,49 @@
1
+ module CoreExt
2
+ module NumberHelper
3
+ class NumberToPhoneConverter < NumberConverter #:nodoc:
4
+ def convert
5
+ str = country_code(opts[:country_code])
6
+ str << convert_to_phone_number(number.to_s.strip)
7
+ str << phone_ext(opts[:extension])
8
+ end
9
+
10
+ private
11
+
12
+ def convert_to_phone_number(number)
13
+ if opts[:area_code]
14
+ convert_with_area_code(number)
15
+ else
16
+ convert_without_area_code(number)
17
+ end
18
+ end
19
+
20
+ def convert_with_area_code(number)
21
+ number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3")
22
+ number
23
+ end
24
+
25
+ def convert_without_area_code(number)
26
+ number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
27
+ number.slice!(0, 1) if start_with_delimiter?(number)
28
+ number
29
+ end
30
+
31
+ def start_with_delimiter?(number)
32
+ delimiter.present? && number.start_with?(delimiter)
33
+ end
34
+
35
+ def delimiter
36
+ opts[:delimiter] || "-"
37
+ end
38
+
39
+ def country_code(code)
40
+ code.blank? ? "" : "+#{code}#{delimiter}"
41
+ end
42
+
43
+ def phone_ext(ext)
44
+ ext.blank? ? "" : " x #{ext}"
45
+ end
46
+ end
47
+ end
48
+ end
49
+