mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-25 13:07:02 +00:00
345 lines
12 KiB
Plaintext
345 lines
12 KiB
Plaintext
/*
|
|
* CPNumberFormatter.j
|
|
* Foundation
|
|
*
|
|
* Created by Alexander Ljungberg.
|
|
* Copyright 2011, WireLoad Inc.
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
@import "CPString.j"
|
|
@import "CPFormatter.j"
|
|
@import "CPDecimalNumber.j"
|
|
|
|
@typedef CPNumberFormatterStyle
|
|
CPNumberFormatterNoStyle = 0;
|
|
CPNumberFormatterDecimalStyle = 1;
|
|
CPNumberFormatterCurrencyStyle = 2;
|
|
CPNumberFormatterPercentStyle = 3;
|
|
CPNumberFormatterScientificStyle = 4;
|
|
CPNumberFormatterSpellOutStyle = 5;
|
|
|
|
@typedef CPNumberFormatterRoundingMode
|
|
CPNumberFormatterRoundCeiling = CPRoundUp;
|
|
CPNumberFormatterRoundFloor = CPRoundDown;
|
|
CPNumberFormatterRoundDown = CPRoundDown;
|
|
CPNumberFormatterRoundUp = CPRoundUp;
|
|
CPNumberFormatterRoundHalfEven = CPRoundBankers;
|
|
CPNumberFormatterRoundHalfDown = _CPRoundHalfDown;
|
|
CPNumberFormatterRoundHalfUp = CPRoundPlain;
|
|
|
|
var NumberRegex = new RegExp('(-)?(\\d*)(\\.(\\d*))?');
|
|
|
|
#define SET_NEEDS_NUMBER_HANDLER_UPDATE() _numberHandler = nil
|
|
|
|
|
|
/*!
|
|
@ingroup foundation
|
|
@class CPNumberFormatter
|
|
|
|
CPNumberFormatter takes a numeric CPNumber value and formats it as text for
|
|
display. It also supports the converse, taking text and interpreting it as a
|
|
CPNumber by configurable formatting rules.
|
|
*/
|
|
@implementation CPNumberFormatter : CPFormatter
|
|
{
|
|
CPNumberFormatterStyle _numberStyle @accessors(property=numberStyle);
|
|
CPString _perMillSymbol @accessors(property=perMillSymbol);
|
|
CPString _groupingSeparator @accessors(property=groupingSeparator);
|
|
CPNumberFormatterRoundingMode _roundingMode @accessors(property=roundingMode);
|
|
CPUInteger _minimumFractionDigits @accessors(property=minimumFractionDigits);
|
|
CPUInteger _maximumFractionDigits @accessors(property=maximumFractionDigits);
|
|
CPUInteger _minimum @accessors(property=minimum);
|
|
CPUInteger _maximum @accessors(property=maximum);
|
|
CPString _currencyCode @accessors(property=currencyCode);
|
|
CPString _currencySymbol @accessors(property=currencySymbol);
|
|
BOOL _generatesDecimalNumbers @accessors(property=generatesDecimalNumbers);
|
|
|
|
// Note that we do not implement the 10.0 style number formatter, but the 10.4+ formatter. Therefore
|
|
// we don't expose this through a `roundingBehavior` property.
|
|
CPDecimalNumberHandler _numberHandler;
|
|
}
|
|
|
|
- (id)init
|
|
{
|
|
if (self = [super init])
|
|
{
|
|
_roundingMode = CPNumberFormatterRoundHalfEven;
|
|
_minimumFractionDigits = 0;
|
|
_maximumFractionDigits = 0;
|
|
_groupingSeparator = @",";
|
|
_generatesDecimalNumbers = YES;
|
|
_minimum = nil;
|
|
_maximum = nil;
|
|
|
|
// FIXME Add locale support.
|
|
_currencyCode = @"USD";
|
|
_currencySymbol = @"$";
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (CPString)stringFromNumber:(CPNumber)number
|
|
{
|
|
if (_numberStyle == CPNumberFormatterPercentStyle)
|
|
{
|
|
number *= 100.0;
|
|
}
|
|
|
|
var dcmn = [number isKindOfClass:CPDecimalNumber] ? number : [[CPDecimalNumber alloc] _initWithJSNumber:number];
|
|
|
|
// TODO Add locale support.
|
|
switch (_numberStyle)
|
|
{
|
|
case CPNumberFormatterCurrencyStyle:
|
|
case CPNumberFormatterDecimalStyle:
|
|
case CPNumberFormatterPercentStyle:
|
|
[self _updateNumberHandlerIfNecessary];
|
|
|
|
dcmn = [dcmn decimalNumberByRoundingAccordingToBehavior:_numberHandler];
|
|
|
|
var output = [dcmn descriptionWithLocale:nil],
|
|
// FIXME this is probably locale dependent.
|
|
parts = output.match(NumberRegex) || ["", undefined, "", undefined, undefined],
|
|
negativePrefix = parts[1] || "",
|
|
preFraction = parts[2] || "",
|
|
fraction = parts[4] || "",
|
|
preFractionLength = [preFraction length],
|
|
commaPosition = 3;
|
|
|
|
while (fraction.length < _minimumFractionDigits)
|
|
fraction += "0";
|
|
|
|
// TODO This is just a temporary solution. Should be generalised.
|
|
// Add in thousands separators.
|
|
if (_groupingSeparator)
|
|
{
|
|
for (var commaPosition = 3, prefLength = [preFraction length]; commaPosition < prefLength; commaPosition += 4)
|
|
{
|
|
preFraction = [preFraction stringByReplacingCharactersInRange:CPMakeRange(prefLength - commaPosition, 0) withString:_groupingSeparator];
|
|
prefLength += 1;
|
|
}
|
|
}
|
|
|
|
var string = preFraction;
|
|
|
|
if (fraction)
|
|
string += "." + fraction;
|
|
|
|
if (_numberStyle === CPNumberFormatterCurrencyStyle)
|
|
{
|
|
if (_currencySymbol)
|
|
string = _currencySymbol + string;
|
|
else
|
|
string = _currencyCode + string;
|
|
}
|
|
|
|
if (_numberStyle == CPNumberFormatterPercentStyle)
|
|
string += "%";
|
|
|
|
if (negativePrefix)
|
|
string = negativePrefix + string;
|
|
|
|
return string;
|
|
|
|
default:
|
|
return [number description];
|
|
}
|
|
}
|
|
|
|
+ (CPString)localizedStringFromNumber:(CPNumber)num numberStyle:(CPNumberFormatterStyle)localizationStyle
|
|
{
|
|
var formatter = [[CPNumberFormatter alloc] init];
|
|
[formatter setNumberStyle:localizationStyle];
|
|
|
|
return [formatter stringFromNumber:num];
|
|
}
|
|
|
|
- (CPNumber)numberFromString:(CPString)aString
|
|
{
|
|
if (_generatesDecimalNumbers)
|
|
return [CPDecimalNumber decimalNumberWithString:aString];
|
|
else
|
|
return parseFloat(aString);
|
|
}
|
|
|
|
- (CPString)stringForObjectValue:(id)anObject
|
|
{
|
|
if ([anObject isKindOfClass:[CPNumber class]])
|
|
return [self stringFromNumber:anObject];
|
|
else
|
|
return [anObject description];
|
|
}
|
|
|
|
- (CPString)editingStringForObjectValue:(id)anObject
|
|
{
|
|
return [self stringForObjectValue:anObject];
|
|
}
|
|
|
|
- (BOOL)getObjectValue:(idRef)anObjectRef forString:(CPString)aString errorDescription:(CPStringRef)anErrorRef
|
|
{
|
|
// Interpret an empty string as nil, like in Cocoa.
|
|
if (aString === @"")
|
|
{
|
|
@deref(anObjectRef) = nil;
|
|
return YES;
|
|
}
|
|
|
|
var value = [self numberFromString:aString],
|
|
error = @"";
|
|
|
|
// this will return false if we've received anything but a number, most likely NaN
|
|
if (!isFinite(value))
|
|
error = @"Value is not a number";
|
|
else if (_minimum != nil && value < _minimum)
|
|
error = @"Value is less than the minimum allowed value";
|
|
else if (_maximum != nil && value > _maximum)
|
|
error = @"Value is greater than the maximum allowed value";
|
|
|
|
if (error)
|
|
{
|
|
if (anErrorRef)
|
|
@deref(anErrorRef) = error;
|
|
|
|
return NO;
|
|
}
|
|
|
|
@deref(anObjectRef) = value;
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)setNumberStyle:(CPNumberFormatterStyle)aStyle
|
|
{
|
|
_numberStyle = aStyle;
|
|
|
|
switch (aStyle)
|
|
{
|
|
case CPNumberFormatterDecimalStyle:
|
|
_minimumFractionDigits = 0;
|
|
_maximumFractionDigits = 3;
|
|
SET_NEEDS_NUMBER_HANDLER_UPDATE();
|
|
break;
|
|
|
|
case CPNumberFormatterCurrencyStyle:
|
|
_minimumFractionDigits = 2;
|
|
_maximumFractionDigits = 2;
|
|
SET_NEEDS_NUMBER_HANDLER_UPDATE();
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (void)setRoundingMode:(CPNumberFormatterRoundingMode)aRoundingMode
|
|
{
|
|
_roundingMode = aRoundingMode;
|
|
SET_NEEDS_NUMBER_HANDLER_UPDATE();
|
|
}
|
|
|
|
- (void)setMinimumFractionDigits:(CPUInteger)aNumber
|
|
{
|
|
_minimumFractionDigits = aNumber;
|
|
SET_NEEDS_NUMBER_HANDLER_UPDATE();
|
|
}
|
|
|
|
- (void)setMaximumFractionDigits:(CPUInteger)aNumber
|
|
{
|
|
_maximumFractionDigits = aNumber;
|
|
SET_NEEDS_NUMBER_HANDLER_UPDATE();
|
|
}
|
|
|
|
- (void)setMinimum:(CPUInteger)aNumber
|
|
{
|
|
_minimum = aNumber;
|
|
SET_NEEDS_NUMBER_HANDLER_UPDATE();
|
|
}
|
|
|
|
- (void)setMaximum:(CPUInteger)aNumber
|
|
{
|
|
_maximum = aNumber;
|
|
SET_NEEDS_NUMBER_HANDLER_UPDATE();
|
|
}
|
|
|
|
#pragma mark Private
|
|
|
|
- (void)_updateNumberHandlerIfNecessary
|
|
{
|
|
if (!_numberHandler)
|
|
_numberHandler = [CPDecimalNumberHandler decimalNumberHandlerWithRoundingMode:_roundingMode
|
|
scale:_maximumFractionDigits
|
|
raiseOnExactness:NO
|
|
raiseOnOverflow:NO
|
|
raiseOnUnderflow:NO
|
|
raiseOnDivideByZero:YES];
|
|
}
|
|
|
|
@end
|
|
|
|
var CPNumberFormatterStyleKey = @"CPNumberFormatterStyleKey",
|
|
CPNumberFormatterMinimumFractionDigitsKey = @"CPNumberFormatterMinimumFractionDigitsKey",
|
|
CPNumberFormatterMaximumFractionDigitsKey = @"CPNumberFormatterMaximumFractionDigitsKey",
|
|
CPNumberFormatterMinimumKey = @"CPNumberFormatterMinimumKey",
|
|
CPNumberFormatterMaximumKey = @"CPNumberFormatterMaximumKey",
|
|
CPNumberFormatterRoundingModeKey = @"CPNumberFormatterRoundingModeKey",
|
|
CPNumberFormatterGroupingSeparatorKey = @"CPNumberFormatterGroupingSeparatorKey",
|
|
CPNumberFormatterCurrencyCodeKey = @"CPNumberFormatterCurrencyCodeKey",
|
|
CPNumberFormatterCurrencySymbolKey = @"CPNumberFormatterCurrencySymbolKey",
|
|
CPNumberFormatterGeneratesDecimalNumbers = @"CPNumberFormatterGeneratesDecimalNumbers";
|
|
|
|
@implementation CPNumberFormatter (CPCoding)
|
|
|
|
- (id)initWithCoder:(CPCoder)aCoder
|
|
{
|
|
self = [super initWithCoder:aCoder];
|
|
|
|
if (self)
|
|
{
|
|
_numberStyle = [aCoder decodeIntForKey:CPNumberFormatterStyleKey];
|
|
_minimumFractionDigits = [aCoder decodeIntForKey:CPNumberFormatterMinimumFractionDigitsKey];
|
|
_maximumFractionDigits = [aCoder decodeIntForKey:CPNumberFormatterMaximumFractionDigitsKey];
|
|
_roundingMode = [aCoder decodeIntForKey:CPNumberFormatterRoundingModeKey];
|
|
_groupingSeparator = [aCoder decodeObjectForKey:CPNumberFormatterGroupingSeparatorKey];
|
|
_currencyCode = [aCoder decodeObjectForKey:CPNumberFormatterCurrencyCodeKey];
|
|
_currencySymbol = [aCoder decodeObjectForKey:CPNumberFormatterCurrencySymbolKey];
|
|
_generatesDecimalNumbers = [aCoder decodeBoolForKey:CPNumberFormatterGeneratesDecimalNumbers];
|
|
|
|
// We decode _minimum and _maximum as object here because otherwise, nil values are not preserved
|
|
// causing a min and max always set to 0 after an decoding.
|
|
_minimum = [aCoder decodeObjectForKey:CPNumberFormatterMinimumKey];
|
|
_maximum = [aCoder decodeObjectForKey:CPNumberFormatterMaximumKey];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)encodeWithCoder:(CPCoder)aCoder
|
|
{
|
|
[super encodeWithCoder:aCoder];
|
|
|
|
[aCoder encodeInt:_numberStyle forKey:CPNumberFormatterStyleKey];
|
|
[aCoder encodeInt:_minimumFractionDigits forKey:CPNumberFormatterMinimumFractionDigitsKey];
|
|
[aCoder encodeInt:_maximumFractionDigits forKey:CPNumberFormatterMaximumFractionDigitsKey];
|
|
[aCoder encodeInt:_minimum forKey:CPNumberFormatterMinimumKey];
|
|
[aCoder encodeInt:_maximum forKey:CPNumberFormatterMaximumKey];
|
|
[aCoder encodeInt:_roundingMode forKey:CPNumberFormatterRoundingModeKey];
|
|
[aCoder encodeObject:_groupingSeparator forKey:CPNumberFormatterGroupingSeparatorKey];
|
|
[aCoder encodeObject:_currencyCode forKey:CPNumberFormatterCurrencyCodeKey];
|
|
[aCoder encodeObject:_currencySymbol forKey:CPNumberFormatterCurrencySymbolKey];
|
|
[aCoder encodeBool:_generatesDecimalNumbers forKey:CPNumberFormatterGeneratesDecimalNumbers];
|
|
}
|
|
|
|
@end
|