Cappuccino  1.0.0
 All Classes Files Functions Variables Typedefs Macros Groups Pages
CPNumberFormatter.j
Go to the documentation of this file.
1 /*
2  * CPNumberFormatter.j
3  * Foundation
4  *
5  * Created by Alexander Ljungberg.
6  * Copyright 2011, WireLoad Inc.
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Lesser General Public
10  * License as published by the Free Software Foundation; either
11  * version 2.1 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16  * Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public
19  * License along with this library; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21  */
22 
23 
24 @typedef CPNumberFormatterStyle
31 
32 @typedef CPNumberFormatterRoundingMode
38 CPNumberFormatterRoundHalfDown = _CPRoundHalfDown;
40 
41 var NumberRegex = new RegExp('(-)?(\\d*)(\\.(\\d*))?');
42 
43 #define SET_NEEDS_NUMBER_HANDLER_UPDATE() _numberHandler = nil
44 
45 
53 @implementation CPNumberFormatter : CPFormatter
54 {
55  CPNumberFormatterStyle _numberStyle;
56  CPString _perMillSymbol;
57  CPString _groupingSeparator;
58  CPNumberFormatterRoundingMode _roundingMode;
59  CPUInteger _minimumFractionDigits;
60  CPUInteger _maximumFractionDigits;
61  CPUInteger _minimum;
62  CPUInteger _maximum;
63  CPString _currencyCode;
64  CPString _currencySymbol;
65  BOOL _generatesDecimalNumbers;
66 
67  // Note that we do not implement the 10.0 style number formatter, but the 10.4+ formatter. Therefore
68  // we don't expose this through a `roundingBehavior` property.
69  CPDecimalNumberHandler _numberHandler;
70 }
71 
72 - (id)init
73 {
74  if (self = [super init])
75  {
76  _roundingMode = CPNumberFormatterRoundHalfEven;
77  _minimumFractionDigits = 0;
78  _maximumFractionDigits = 0;
79  _groupingSeparator = @",";
80  _generatesDecimalNumbers = YES;
81  _minimum = nil;
82  _maximum = nil;
83 
84  // FIXME Add locale support.
85  _currencyCode = @"USD";
86  _currencySymbol = @"$";
87  }
88 
89  return self;
90 }
91 
92 - (CPString)stringFromNumber:(CPNumber)number
93 {
94  if (_numberStyle == CPNumberFormatterPercentStyle)
95  {
96  number *= 100.0;
97  }
98 
99  var dcmn = [number isKindOfClass:CPDecimalNumber] ? number : [[CPDecimalNumber alloc] _initWithJSNumber:number];
100 
101  // TODO Add locale support.
102  switch (_numberStyle)
103  {
107  [self _updateNumberHandlerIfNecessary];
108 
109  dcmn = [dcmn decimalNumberByRoundingAccordingToBehavior:_numberHandler];
110 
111  var output = [dcmn descriptionWithLocale:nil],
112  // FIXME this is probably locale dependent.
113  parts = output.match(NumberRegex) || ["", undefined, "", undefined, undefined],
114  negativePrefix = parts[1] || "",
115  preFraction = parts[2] || "",
116  fraction = parts[4] || "",
117  preFractionLength = [preFraction length],
118  commaPosition = 3;
119 
120  while (fraction.length < _minimumFractionDigits)
121  fraction += "0";
122 
123  // TODO This is just a temporary solution. Should be generalised.
124  // Add in thousands separators.
125  if (_groupingSeparator)
126  {
127  for (var commaPosition = 3, prefLength = [preFraction length]; commaPosition < prefLength; commaPosition += 4)
128  {
129  preFraction = [preFraction stringByReplacingCharactersInRange:CPMakeRange(prefLength - commaPosition, 0) withString:_groupingSeparator];
130  prefLength += 1;
131  }
132  }
133 
134  var string = preFraction;
135 
136  if (fraction)
137  string += "." + fraction;
138 
139  if (_numberStyle === CPNumberFormatterCurrencyStyle)
140  {
141  if (_currencySymbol)
142  string = _currencySymbol + string;
143  else
144  string = _currencyCode + string;
145  }
146 
147  if (_numberStyle == CPNumberFormatterPercentStyle)
148  string += "%";
149 
150  if (negativePrefix)
151  string = negativePrefix + string;
152 
153  return string;
154 
155  default:
156  return [number description];
157  }
158 }
159 
160 + (CPString)localizedStringFromNumber:(CPNumber)num numberStyle:(CPNumberFormatterStyle)localizationStyle
161 {
162  var formatter = [[CPNumberFormatter alloc] init];
163  [formatter setNumberStyle:localizationStyle];
164 
165  return [formatter stringFromNumber:num];
166 }
167 
168 - (CPNumber)numberFromString:(CPString)aString
169 {
170  if (_generatesDecimalNumbers)
171  return [CPDecimalNumber decimalNumberWithString:aString];
172  else
173  return parseFloat(aString);
174 }
175 
176 - (CPString)stringForObjectValue:(id)anObject
177 {
178  if ([anObject isKindOfClass:[CPNumber class]])
179  return [self stringFromNumber:anObject];
180  else
181  return [anObject description];
182 }
183 
184 - (CPString)editingStringForObjectValue:(id)anObject
185 {
186  return [self stringForObjectValue:anObject];
187 }
188 
189 - (BOOL)getObjectValue:(idRef)anObjectRef forString:(CPString)aString errorDescription:(CPStringRef)anErrorRef
190 {
191  // Interpret an empty string as nil, like in Cocoa.
192  if (aString === @"")
193  {
194  @deref(anObjectRef) = nil;
195  return YES;
196  }
197 
198  var value = [self numberFromString:aString],
199  error = @"";
200 
201  // this will return false if we've received anything but a number, most likely NaN
202  if (!isFinite(value))
203  error = @"Value is not a number";
204  else if (_minimum !== nil && value < _minimum)
205  error = @"Value is less than the minimum allowed value";
206  else if (_maximum !== nil && value > _maximum)
207  error = @"Value is greater than the maximum allowed value";
208 
209  if (error)
210  {
211  if (anErrorRef)
212  @deref(anErrorRef) = error;
213 
214  return NO;
215  }
216 
217  @deref(anObjectRef) = value;
218 
219  return YES;
220 }
221 
222 - (void)setNumberStyle:(CPNumberFormatterStyle)aStyle
223 {
224  _numberStyle = aStyle;
225 
226  switch (aStyle)
227  {
229  _minimumFractionDigits = 0;
230  _maximumFractionDigits = 3;
232  break;
233 
235  _minimumFractionDigits = 2;
236  _maximumFractionDigits = 2;
238  break;
239  }
240 }
241 
242 - (void)setRoundingMode:(CPNumberFormatterRoundingMode)aRoundingMode
243 {
244  _roundingMode = aRoundingMode;
246 }
247 
248 - (void)setMinimumFractionDigits:(CPUInteger)aNumber
249 {
250  _minimumFractionDigits = aNumber;
252 }
253 
254 - (void)setMaximumFractionDigits:(CPUInteger)aNumber
255 {
256  _maximumFractionDigits = aNumber;
258 }
259 
260 - (void)setMinimum:(CPUInteger)aNumber
261 {
262  _minimum = aNumber;
264 }
265 
266 - (void)setMaximum:(CPUInteger)aNumber
267 {
268  _maximum = aNumber;
270 }
271 
272 #pragma mark Private
273 
274 - (void)_updateNumberHandlerIfNecessary
275 {
276  if (!_numberHandler)
277  _numberHandler = [CPDecimalNumberHandler decimalNumberHandlerWithRoundingMode:_roundingMode
278  scale:_maximumFractionDigits
280  raiseOnOverflow:NO
282  raiseOnDivideByZero:YES];
283 }
284 
285 @end
286 
287 var CPNumberFormatterStyleKey = @"CPNumberFormatterStyleKey",
288  CPNumberFormatterMinimumFractionDigitsKey = @"CPNumberFormatterMinimumFractionDigitsKey",
289  CPNumberFormatterMaximumFractionDigitsKey = @"CPNumberFormatterMaximumFractionDigitsKey",
290  CPNumberFormatterMinimumKey = @"CPNumberFormatterMinimumKey",
291  CPNumberFormatterMaximumKey = @"CPNumberFormatterMaximumKey",
292  CPNumberFormatterRoundingModeKey = @"CPNumberFormatterRoundingModeKey",
293  CPNumberFormatterGroupingSeparatorKey = @"CPNumberFormatterGroupingSeparatorKey",
294  CPNumberFormatterCurrencyCodeKey = @"CPNumberFormatterCurrencyCodeKey",
295  CPNumberFormatterCurrencySymbolKey = @"CPNumberFormatterCurrencySymbolKey",
296  CPNumberFormatterGeneratesDecimalNumbers = @"CPNumberFormatterGeneratesDecimalNumbers";
297 
299 
300 - (id)initWithCoder:(CPCoder)aCoder
301 {
302  self = [super initWithCoder:aCoder];
303 
304  if (self)
305  {
306  _numberStyle = [aCoder decodeIntForKey:CPNumberFormatterStyleKey];
307  _minimumFractionDigits = [aCoder decodeIntForKey:CPNumberFormatterMinimumFractionDigitsKey];
308  _maximumFractionDigits = [aCoder decodeIntForKey:CPNumberFormatterMaximumFractionDigitsKey];
309  _roundingMode = [aCoder decodeIntForKey:CPNumberFormatterRoundingModeKey];
310  _groupingSeparator = [aCoder decodeObjectForKey:CPNumberFormatterGroupingSeparatorKey];
311  _currencyCode = [aCoder decodeObjectForKey:CPNumberFormatterCurrencyCodeKey];
312  _currencySymbol = [aCoder decodeObjectForKey:CPNumberFormatterCurrencySymbolKey];
313  _generatesDecimalNumbers = [aCoder decodeBoolForKey:CPNumberFormatterGeneratesDecimalNumbers];
314 
315  // We decode _minimum and _maximum as object here because otherwise, nil values are not preserved
316  // causing a min and max always set to 0 after an decoding.
317  _minimum = [aCoder decodeObjectForKey:CPNumberFormatterMinimumKey];
318  _maximum = [aCoder decodeObjectForKey:CPNumberFormatterMaximumKey];
319  }
320 
321  return self;
322 }
323 
324 - (void)encodeWithCoder:(CPCoder)aCoder
325 {
326  [super encodeWithCoder:aCoder];
327 
328  [aCoder encodeInt:_numberStyle forKey:CPNumberFormatterStyleKey];
329  [aCoder encodeInt:_minimumFractionDigits forKey:CPNumberFormatterMinimumFractionDigitsKey];
330  [aCoder encodeInt:_maximumFractionDigits forKey:CPNumberFormatterMaximumFractionDigitsKey];
331  [aCoder encodeInt:_minimum forKey:CPNumberFormatterMinimumKey];
332  [aCoder encodeInt:_maximum forKey:CPNumberFormatterMaximumKey];
333  [aCoder encodeInt:_roundingMode forKey:CPNumberFormatterRoundingModeKey];
334  [aCoder encodeObject:_groupingSeparator forKey:CPNumberFormatterGroupingSeparatorKey];
335  [aCoder encodeObject:_currencyCode forKey:CPNumberFormatterCurrencyCodeKey];
336  [aCoder encodeObject:_currencySymbol forKey:CPNumberFormatterCurrencySymbolKey];
337  [aCoder encodeBool:_generatesDecimalNumbers forKey:CPNumberFormatterGeneratesDecimalNumbers];
338 }
339 
340 @end
341 
343 
347 - (CPNumberFormatterStyle)numberStyle
348 {
349  return _numberStyle;
350 }
351 
355 - (void)setNumberStyle:(CPNumberFormatterStyle)aValue
356 {
357  _numberStyle = aValue;
358 }
359 
363 - (CPString)perMillSymbol
364 {
365  return _perMillSymbol;
366 }
367 
371 - (void)setPerMillSymbol:(CPString)aValue
372 {
373  _perMillSymbol = aValue;
374 }
375 
379 - (CPString)groupingSeparator
380 {
381  return _groupingSeparator;
382 }
383 
387 - (void)setGroupingSeparator:(CPString)aValue
388 {
389  _groupingSeparator = aValue;
390 }
391 
395 - (CPNumberFormatterRoundingMode)roundingMode
396 {
397  return _roundingMode;
398 }
399 
403 - (void)setRoundingMode:(CPNumberFormatterRoundingMode)aValue
404 {
405  _roundingMode = aValue;
406 }
407 
411 - (CPUInteger)minimumFractionDigits
412 {
413  return _minimumFractionDigits;
414 }
415 
419 - (void)setMinimumFractionDigits:(CPUInteger)aValue
420 {
421  _minimumFractionDigits = aValue;
422 }
423 
427 - (CPUInteger)maximumFractionDigits
428 {
429  return _maximumFractionDigits;
430 }
431 
435 - (void)setMaximumFractionDigits:(CPUInteger)aValue
436 {
437  _maximumFractionDigits = aValue;
438 }
439 
443 - (CPUInteger)minimum
444 {
445  return _minimum;
446 }
447 
451 - (void)setMinimum:(CPUInteger)aValue
452 {
453  _minimum = aValue;
454 }
455 
459 - (CPUInteger)maximum
460 {
461  return _maximum;
462 }
463 
467 - (void)setMaximum:(CPUInteger)aValue
468 {
469  _maximum = aValue;
470 }
471 
475 - (CPString)currencyCode
476 {
477  return _currencyCode;
478 }
479 
483 - (void)setCurrencyCode:(CPString)aValue
484 {
485  _currencyCode = aValue;
486 }
487 
491 - (CPString)currencySymbol
492 {
493  return _currencySymbol;
494 }
495 
499 - (void)setCurrencySymbol:(CPString)aValue
500 {
501  _currencySymbol = aValue;
502 }
503 
507 - (BOOL)generatesDecimalNumbers
508 {
509  return _generatesDecimalNumbers;
510 }
511 
515 - (void)setGeneratesDecimalNumbers:(BOOL)aValue
516 {
517  _generatesDecimalNumbers = aValue;
518 }
519 
520 @end