Changesets can be listed by changeset number.
The Git repository is here.
- Revision:
- 390
- Log:
Initial import of Canvass, a donations-based poll application.
- Author:
- rool
- Date:
- Mon Mar 21 14:58:04 +0000 2011
- Size:
- 13687 Bytes
1 | ######################################################################## |
2 | # File:: currency.rb |
3 | # (C):: Hipposoft 2009, 2010, 2011 |
4 | # |
5 | # Purpose:: Record details of a currency including the fiscally correct |
6 | # way to round its values (dependent upon a hard-coded choice |
7 | # of named rounding algorithms coded herein). |
8 | # ---------------------------------------------------------------------- |
9 | # 14-Apr-2009 (ADH): Created. |
10 | # 18-Feb-2011 (ADH): Imported from Artisan. |
11 | ######################################################################## |
12 | |
13 | require 'bigdecimal' |
14 | |
15 | class Currency < ActiveRecord::Base |
16 | |
17 | # =========================================================================== |
18 | # CHARACTERISTICS |
19 | # =========================================================================== |
20 | |
21 | has_many :polls |
22 | has_many :donations |
23 | |
24 | # Limitations and requirements. See also the rounding algorithms section. |
25 | |
26 | MAXLEN_NAME = 160 # "Native name (English name)" |
27 | MAXLEN_CODE = 3 # ISO 4217 => 3-letter codes |
28 | |
29 | MAXLEN_INTEGER_TEMPLATE = 32 # Fairly arbitrary - allows big integers (e.g. for Yen) |
30 | MAXLEN_DELIMITER = 8 # Usually just 1; 8 is a bit arbitrary |
31 | MAXLEN_FRACTION_TEMPLATE = 16 # Fairly arbitrary - allows long fractions/non-numerics in fractions |
32 | |
33 | MAXLEN_SYMBOL = 16 # Often just 1; 16 is a bit arbitrary |
34 | |
35 | validates_presence_of :name |
36 | validates_numericality_of :decimal_precision, :only_integer => true |
37 | |
38 | # Default values (should only be needed in the migrations, but available |
39 | # centrally here just in case). See also the rounding algorithms section. |
40 | # |
41 | # Decimal precision refers to *internal* rounding before the currency |
42 | # specific rounding algorithm is used. So for example, Swiss Francs would |
43 | # use a precision of 4, because the rounding algorithm works on the last |
44 | # two of four decimal digits. The currency-specific rounded result will |
45 | # always have equal or lower decimal precision. |
46 | |
47 | DEFAULT_DECIMAL_PRECISION = 2 |
48 | DEFAULT_SHOW_AFTER_NUMBER = false |
49 | |
50 | # See Jason King's "good_sort" plugin: |
51 | # |
52 | # http://github.com/JasonKing/good_sort/tree/master |
53 | # |
54 | # Must use "table_exists?", as good_sort needs to check the database but |
55 | # this class may be examined by migrations before the table is created. |
56 | |
57 | sort_on :name, :code, :symbol, :rounding_algorithm if Currency.table_exists? |
58 | |
59 | # How many entries to list per index page? See the Will Paginate plugin: |
60 | # |
61 | # http://wiki.github.com/mislav/will_paginate |
62 | |
63 | def self.per_page |
64 | MAXIMUM_LIST_ITEMS_PER_PAGE |
65 | end |
66 | |
67 | # Search columns for views rendering the "shared/_simple_search.html.erb" |
68 | # view partial and using "appctrl_build_search_conditions" to handle queries. |
69 | |
70 | SEARCH_COLUMNS = %w{name code symbol rounding_algorithm} |
71 | |
72 | # =========================================================================== |
73 | # PERMISSIONS |
74 | # =========================================================================== |
75 | |
76 | # Only administrators and agents can modify the currency list. |
77 | # |
78 | def self.can_modify?( user, ignored ) |
79 | user.try( :is_agent? ) |
80 | end |
81 | |
82 | # =========================================================================== |
83 | # ROUNDING ALGORITHMS - all take strings in, return strings out; returned |
84 | # strings are formatted according to current locale numeric formatting |
85 | # conventions and to "decimal_precision" places (subject to the rounding |
86 | # algorithm details; Argentinian rounding uses a precision of 3 but may |
87 | # return values quoted only to 2 decimal places). |
88 | # |
89 | # IMPORTANT - Keys are matched against rounding algorithm strings in the |
90 | # database. Maximum key string length is MAXLEN_ROUNDING_ALGORITHM, |
91 | # defined further below. DEFAULT_ROUNDING_ALGORITHM gives the value to use |
92 | # if all else fails. |
93 | # |
94 | # The order of appearance herein has no relevance. Since the constant defines |
95 | # a hash, Ruby may choose to enumerate the key/value pairs in any order. Use |
96 | # ROUNDING_ALGORITHM_ORDER to establish a default sort order. |
97 | # =========================================================================== |
98 | |
99 | ROUNDING_ALGORITHM_ORDER = [ |
100 | |
101 | 'mathematical', # 0 |
102 | 'round_up', # 1 |
103 | 'round_down', # 2 |
104 | 'argentinian', # 3 |
105 | 'swiss' # 4 |
106 | |
107 | ] |
108 | |
109 | validates_inclusion_of :rounding_algorithm, :in => ROUNDING_ALGORITHM_ORDER |
110 | |
111 | ROUNDING_ALGORITHMS = { |
112 | ROUNDING_ALGORITHM_ORDER[ 0 ] => { |
113 | # Mathematical |
114 | :hint => 'ax: x < 5 -> x=0, x > 4 -> a+1, x=0', |
115 | :proc => Proc.new do | currency, string | |
116 | currency.rounding_internal( string, :round ) |
117 | end |
118 | }, |
119 | |
120 | ROUNDING_ALGORITHM_ORDER[ 1 ] => { |
121 | # Round up |
122 | :hint => 'ax: x -> a+1, x=0', |
123 | :proc => Proc.new do | currency, string | |
124 | currency.rounding_internal( string, :floor ) |
125 | end |
126 | }, |
127 | |
128 | ROUNDING_ALGORITHM_ORDER[ 2 ] => { |
129 | # Round down |
130 | :hint => 'ax: x -> x=0', |
131 | :proc => Proc.new do | currency, string | |
132 | currency.rounding_internal( string, :ceil ) |
133 | end |
134 | }, |
135 | |
136 | ROUNDING_ALGORITHM_ORDER[ 3 ] => { |
137 | # Argentinian |
138 | :hint => '0.0ax: x < 3 -> x=0; 2 < x < 8 -> x=5; x > 7 -> a+1, x=0', |
139 | :proc => Proc.new do | currency, string | |
140 | currency.rounding_argentinian( string ) |
141 | end |
142 | }, |
143 | |
144 | ROUNDING_ALGORITHM_ORDER[ 4 ] => { |
145 | # Swiss |
146 | :hint => '0.axy: xy < 26 -> xy=00; 25 < x < 76 -> xy=50; xy > 75 -> a+1, xy=00', |
147 | :proc => Proc.new do | currency, string | |
148 | currency.rounding_swiss( string ) |
149 | end |
150 | } |
151 | } |
152 | |
153 | # Maximum length of an algorithm name - used e.g. by the "create currencies" |
154 | # migration; default algorithm choice. |
155 | |
156 | MAXLEN_ROUNDING_ALGORITHM = 32 |
157 | DEFAULT_ROUNDING_ALGORITHM = ROUNDING_ALGORITHM_ORDER[ 0 ] |
158 | |
159 | # =========================================================================== |
160 | # GENERAL |
161 | # =========================================================================== |
162 | |
163 | # Apply a default sort to the given array of Currency objects. The array is |
164 | # modified in place. |
165 | # |
166 | def self.apply_default_sort_order( array ) |
167 | array.sort! { | x, y | x.name <=> y.name } |
168 | end |
169 | |
170 | # Canvass doesn't use Locations like Artisan. |
171 | # |
172 | # # For first-time database setup and tests - establish static associations |
173 | # # between currencies and locations. |
174 | # # |
175 | # def self.establish_predefined_associations |
176 | # assoc = { |
177 | # 'United Kingdom' => 'GBP', |
178 | # 'France' => 'EUR', |
179 | # 'Germany' => 'EUR', |
180 | # 'Switzerland' => 'SFR', |
181 | # 'Canada' => 'CAD', |
182 | # 'Japan' => 'JPY', |
183 | # 'United States of America' => 'USD' |
184 | # } |
185 | # |
186 | # assoc.each do | name, code | |
187 | # location = Location.find( :first, :conditions => [ 'LOWER(name) LIKE LOWER(?)', "%#{ name }%" ] ) |
188 | # currency = Currency.find_by_code( code ) |
189 | # |
190 | # if ( location && currency ) |
191 | # location.currency = currency |
192 | # location.save! |
193 | # end |
194 | # end |
195 | # end |
196 | |
197 | # Try to find the best currency to use by default for the given user. If the |
198 | # user has no associated location or 'nil' is passed, always returns GBP - |
199 | # for artwork searches etc., the default currency used in the absense of a |
200 | # product's user's location needs to be fixed and known in advance. |
201 | # |
202 | def self.get_best_currency( user ) |
203 | # Canvass doesn't use Locations like Artisan. |
204 | # |
205 | # if ( user && user.location && user.location.currency ) |
206 | # return user.location.currency |
207 | # else |
208 | return Currency.find_by_code( 'GBP' ) || Currency.first |
209 | # end |
210 | end |
211 | |
212 | # Given an integer and fraction string, return a single string with all |
213 | # non-numeric characters removed and a "." separating the integer and |
214 | # fraction parts (or with no "." if the fraction part is empty). |
215 | # |
216 | def self.simplify( integer, fraction ) |
217 | integer = clean( integer ) |
218 | fraction = clean( fraction ) |
219 | |
220 | return ( fraction.empty? ) ? integer : "#{ integer }.#{ fraction }" |
221 | end |
222 | |
223 | # Add two numbers, expressed in integers or strings describing the integer |
224 | # and fraction components of each, returning the result in an array of two |
225 | # strings, index 0 holding the integer part and index 1 holding the fraction |
226 | # part. Both are always present (e.g. ["3", "0"] => "3.0" => integer 3). |
227 | # |
228 | def self.add( integer1, fraction1, integer2, fraction2 ) |
229 | big1 = BigDecimal.new( simplify( integer1, fraction1 ) ) |
230 | big2 = BigDecimal.new( simplify( integer2, fraction2 ) ) |
231 | |
232 | return ( big1 + big2 ).to_s( 'F' ).split( '.' ) |
233 | end |
234 | |
235 | # As 'add', but subtracts the value indicated by the second pair of |
236 | # parameters from the value indicated by the first pair of parameters. |
237 | # |
238 | def self.subtract( integer1, fraction1, integer2, fraction2 ) |
239 | big1 = BigDecimal.new( simplify( integer1, fraction1 ) ) |
240 | big2 = BigDecimal.new( simplify( integer2, fraction2 ) ) |
241 | |
242 | return ( big1 - big2 ).to_s( 'F' ).split( '.' ) |
243 | end |
244 | |
245 | # Multiply a number, expressed in integers or strings describing its integer |
246 | # and fraction components, by a multipler, expressed as an integer, float or |
247 | # string equivalent (such that a conversion to BigDecimal will result in the |
248 | # desired mathematical result). Returns the result in the same way as |
249 | # "add()" above. |
250 | # |
251 | # If you already have the multiplier as a BigDecimal, pass it in that format |
252 | # for better efficiency. |
253 | # |
254 | def self.multiply( integer, fraction, multiplier ) |
255 | number = BigDecimal.new( simplify( integer, fraction ) ) |
256 | multiplier = BigDecimal.new( permissive_clean( multiplier ) ) unless ( multiplier.is_a?( BigDecimal ) ) |
257 | |
258 | return ( number * multiplier ).to_s( 'F' ).split( '.' ) |
259 | end |
260 | |
261 | # As 'multiply', but divides by the given divider value returning a result |
262 | # without rounding, within the limits of BigDecimal precision. |
263 | # |
264 | def self.divide( integer, fraction, divider ) |
265 | number = BigDecimal.new( simplify( integer, fraction ) ) |
266 | divider = BigDecimal.new( permissive_clean( divider ) ) unless ( divider.is_a?( BigDecimal ) ) |
267 | |
268 | return ( number / divider ).to_s( 'F' ).split( '.' ) |
269 | end |
270 | |
271 | # Given a string quantity in 'natural' units (e.g. for UK pounds, a |
272 | # string representing a floating point number with integer pounds and |
273 | # fraction pence), return a rounded amount as a string. Rounding is |
274 | # done according to the currency instance's defined rounding algorithm. |
275 | # |
276 | def round( float_as_string ) |
277 | rounding = Currency::ROUNDING_ALGORITHMS[ self.rounding_algorithm ] |
278 | return rounding[ :proc ].call( self, float_as_string ) |
279 | end |
280 | |
281 | # Call with a value string and method of :round, :floor or :ceil. Returns a |
282 | # value appropriate for a rounding function using mathematical, round-down |
283 | # or round-up semantics, respectively. |
284 | # |
285 | def rounding_internal( string, method ) |
286 | bd = BigDecimal.new( string ) |
287 | dp = self.decimal_precision |
288 | |
289 | return "%.0#{ dp }f" % bd.send( method, dp ) |
290 | end |
291 | |
292 | # Works on the third decimal place; < 3, round down; >= 3, < 8, make 5; |
293 | # >= 8, round up. Thus the result may have a third decimal digit value of |
294 | # zero or 5 (i.e. in effect there's a two or three decimal digit result). |
295 | # |
296 | # For valid rounding, decimal precision MUST BE 4. This ensures that |
297 | # mathematical rounding (to four decimal digits) does not influence the |
298 | # value of the third digit. |
299 | # |
300 | def rounding_argentinian( string ) |
301 | str = rounding_internal( string, :round ) |
302 | last_one = str.split( '.' )[ 1 ][ 2..2 ] |
303 | |
304 | if ( last_one < '3' ) |
305 | str = str.chop # Just chop out 3rd decimal place |
306 | elsif ( last_one < '8' ) |
307 | str = str.chop + '5' # Chop out 3rd decimal place and replace with '5' |
308 | else |
309 | str = str.chop.next # Chop 3rd decimal place and round up" |
310 | end |
311 | |
312 | return Currency.trim_to_numeric( str ) |
313 | end |
314 | |
315 | # Similar to Argentinian rounding, but examines the last two digits to |
316 | # always arrive at a 2 decimal place result. Values < 26 => round down, |
317 | # values > 75 => round up, values in between round to '5'. |
318 | # |
319 | # Decimal precision constraints are the same as for Argentinian rounding. |
320 | # |
321 | def rounding_swiss( string ) |
322 | str = rounding_internal( string, :round ) |
323 | last_two = str.split( '.' )[ 1 ][ 1..2 ] |
324 | |
325 | if ( last_two < '26' ) |
326 | str = str.chop.chop.chop + '0' |
327 | elsif ( last_two < '76' ) |
328 | str = str.chop.chop.chop + '5' |
329 | else |
330 | str = str.chop.chop.chop.next + '0' |
331 | end |
332 | |
333 | return Currency.trim_to_numeric( str ) |
334 | end |
335 | |
336 | # =========================================================================== |
337 | # PRIVATE |
338 | # =========================================================================== |
339 | |
340 | private |
341 | |
342 | # Trim any characters outside the range "0-9" from the end of a string. |
343 | # Assumes that numeric values contain these characters only. |
344 | # |
345 | def self.trim_to_numeric( string ) |
346 | string.sub( /[^0-9]+$/, '' ) |
347 | end |
348 | |
349 | # Remove any character which is not a digit, minus sign or dot from the |
350 | # given string. Does nothing about repetitions or position, so if you want |
351 | # to use the result as an integer or float, some degree of input sanity must |
352 | # already be present. |
353 | # |
354 | def self.permissive_clean( string ) |
355 | return string.to_s.gsub( /[^0-9\-.]/, '' ) |
356 | end |
357 | |
358 | # Remove *all* characters that are not digits from the given string. Allow a |
359 | # leading "-" for negative numbers, with optional white space before it, but |
360 | # no other spurious preceding characters. |
361 | # |
362 | def self.clean( string ) |
363 | string = string.gsub( /^(\s)*\-/, "-" ) |
364 | negative = ( string[ 0 ] == 45 ) |
365 | |
366 | string.gsub!( /[^0-9]/, '' ) |
367 | string = "-" + string if ( negative ) |
368 | |
369 | return string |
370 | end |
371 | end |