Files
cappuccino/Foundation/CPNumberFormatter.j
T
David Richardson 02a5434e4f Foundation: remove preprocessor macro from CPNumberFormatter.j
Replace SET_NEEDS_NUMBER_HANDLER_UPDATE() with inline statements.
Add a comment at each of the 6 call sites to state the reason.
No behavior change.
2026-08-04 19:46:09 -06:00

364 lines
13 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*))?');
/*!
@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;
// Invalidate the cached number handler.
// It rebuilds on next use.
// Replaces pre-processor directive, which is incompatible with the new compiler
_numberHandler = nil;
break;
case CPNumberFormatterCurrencyStyle:
_minimumFractionDigits = 2;
_maximumFractionDigits = 2;
// Invalidate the cached number handler.
// It rebuilds on next use.
// Replaces pre-processor directive, which is incompatible with the new compiler
_numberHandler = nil;
break;
}
}
- (void)setRoundingMode:(CPNumberFormatterRoundingMode)aRoundingMode
{
_roundingMode = aRoundingMode;
// Invalidate the cached number handler.
// It rebuilds on next use.
// Replaces pre-processor directive, which is incompatible with the new compiler
_numberHandler = nil;
}
- (void)setMinimumFractionDigits:(CPUInteger)aNumber
{
_minimumFractionDigits = aNumber;
// Invalidate the cached number handler.
// It rebuilds on next use.
// Replaces pre-processor directive, which is incompatible with the new compiler
_numberHandler = nil;
}
- (void)setMaximumFractionDigits:(CPUInteger)aNumber
{
_maximumFractionDigits = aNumber;
// Invalidate the cached number handler.
// It rebuilds on next use.
// Replaces pre-processor directive, which is incompatible with the new compiler
_numberHandler = nil;
}
- (void)setMinimum:(CPUInteger)aNumber
{
_minimum = aNumber;
// Invalidate the cached number handler.
// It rebuilds on next use.
// Replaces pre-processor directive, which is incompatible with the new compiler
_numberHandler = nil;
}
- (void)setMaximum:(CPUInteger)aNumber
{
_maximum = aNumber;
// Invalidate the cached number handler.
// It rebuilds on next use.
// Replaces pre-processor directive, which is incompatible with the new compiler
_numberHandler = nil;
}
// 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