Skip to content

SR-1464: NSNumber.description is not compatible between OS X and linux #1724

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 1, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CoreFoundation/String.subproj/CFString.c
Original file line number Diff line number Diff line change
Expand Up @@ -6013,7 +6013,7 @@ enum {
CFFormatDummyPointerType = 42 /* special case for %n */
};

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_WINDOWS
#if TARGET_OS_MAC || TARGET_OS_WIN32 || TARGET_OS_LINUX
/* Only come in here if spec->type is CFFormatLongType or CFFormatDoubleType. Pass in 0 for width or precision if not specified. Returns false if couldn't do the format (with the assumption the caller falls back to unlocalized).
*/
static Boolean __CFStringFormatLocalizedNumber(CFMutableStringRef output, CFLocaleRef locale, const CFPrintValue *values, const CFFormatSpec *spec, SInt32 width, SInt32 precision, Boolean hasPrecision) {
Expand Down Expand Up @@ -7109,7 +7109,7 @@ static Boolean __CFStringAppendFormatCore(CFMutableStringRef outputString, CFStr
switch (specs[curSpec].type) {
case CFFormatLongType:
case CFFormatDoubleType:
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_WINDOWS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

#if TARGET_OS_MAC || TARGET_OS_WIN32 || TARGET_OS_LINUX
if (localizedFormatting && (specs[curSpec].flags & kCFStringFormatLocalizable)) { // We have a locale, so we do localized formatting
if (__CFStringFormatLocalizedNumber(outputString, (CFLocaleRef)formatOptions, values, &specs[curSpec], width, precision, hasPrecision)) break;
}
Expand Down
6 changes: 5 additions & 1 deletion Foundation/NSDecimalNumber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,11 @@ open class NSDecimalNumber : NSNumber {
public required convenience init(bytes buffer: UnsafeRawPointer, objCType type: UnsafePointer<Int8>) {
NSRequiresConcreteImplementation()
}


open override var description: String {
return self.decimal.description
}

open override func description(withLocale locale: Locale?) -> String {
guard locale == nil else {
fatalError("Locale not supported: \(locale!)")
Expand Down
93 changes: 70 additions & 23 deletions Foundation/NSNumber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,7 @@ open class NSNumber : NSValue {
}

open var stringValue: String {
return description(withLocale: nil)
return self.description
}

/// Create an instance initialized to `value`.
Expand Down Expand Up @@ -958,39 +958,86 @@ open class NSNumber : NSValue {
}
}

private static let _numberFormatterForNilLocale: CFNumberFormatter = {
let formatter: CFNumberFormatter
formatter = CFNumberFormatterCreate(nil, CFLocaleCopyCurrent(), kCFNumberFormatterNoStyle)
CFNumberFormatterSetProperty(formatter, kCFNumberFormatterMaxFractionDigits, 15._bridgeToObjectiveC())
return formatter
}()

open func description(withLocale locale: Locale?) -> String {
// CFNumberFormatterCreateStringWithNumber() does not like numbers of type
// SInt128Type, as it loses the type when looking it up and treats it as
// an SInt64Type, so special case them.
if _CFNumberGetType2(_cfObject) == kCFNumberSInt128Type {
return String(format: "%@", unsafeBitCast(_cfObject, to: UnsafePointer<CFNumber>.self))
}
guard let locale = locale else { return self.description }
switch _CFNumberGetType2(_cfObject) {
case kCFNumberSInt8Type, kCFNumberCharType:
return String(format: "%d", locale: locale, self.int8Value)

let aLocale = locale
let formatter: CFNumberFormatter
if (aLocale == nil) {
formatter = NSNumber._numberFormatterForNilLocale
} else {
formatter = CFNumberFormatterCreate(nil, aLocale?._cfObject, kCFNumberFormatterDecimalStyle)
case kCFNumberSInt16Type, kCFNumberShortType:
return String(format: "%hi", locale: locale, self.int16Value)

case kCFNumberSInt32Type:
return String(format: "%d", locale: locale, self.int32Value)

case kCFNumberIntType, kCFNumberLongType, kCFNumberNSIntegerType, kCFNumberCFIndexType:
return String(format: "%ld", locale: locale, self.intValue)

case kCFNumberSInt64Type, kCFNumberLongLongType:
return String(format: "%lld", locale: locale, self.int64Value)

case kCFNumberSInt128Type:
let value = self.int128Value
if value.high == 0 {
return value.low.description // BUG: "%llu" doesnt work correctly and treats number as signed
} else {
// BUG: Note the locale is actually ignored here as this is converted using CFNumber.c:emit128()
return String(format: "%@", locale: locale, unsafeBitCast(_cfObject, to: UnsafePointer<CFNumber>.self))
}

case kCFNumberFloatType, kCFNumberCGFloatType, kCFNumberFloatType:
return String(format: "%0.7g", locale: locale, self.floatValue)

case kCFNumberFloat64Type, kCFNumberDoubleType:
return String(format: "%0.16g", locale: locale, self.doubleValue)

case kCFNumberCGFloatType:
if Int.max == Int32.max {
return String(format: "%0.7g", locale: locale, self.floatValue)
} else {
return String(format: "%0.16g", locale: locale, self.doubleValue)
}
default: fatalError("Unknown NSNumber Type")
}
return CFNumberFormatterCreateStringWithNumber(nil, formatter, self._cfObject)._swiftObject
}

override open var _cfTypeID: CFTypeID {
return CFNumberGetTypeID()
}

open override var description: String {
return description(withLocale: nil)
switch _CFNumberGetType2(_cfObject) {
case kCFNumberSInt8Type, kCFNumberCharType, kCFNumberSInt16Type, kCFNumberShortType,
kCFNumberSInt32Type, kCFNumberIntType, kCFNumberLongType, kCFNumberNSIntegerType, kCFNumberCFIndexType:
return self.intValue.description

case kCFNumberSInt64Type, kCFNumberLongLongType:
return self.int64Value.description

case kCFNumberSInt128Type:
let value = self.int128Value
if value.high == 0 {
return value.low.description
} else {
return String(format: "%@", locale: nil, unsafeBitCast(_cfObject, to: UnsafePointer<CFNumber>.self))
}

case kCFNumberFloatType, kCFNumberCGFloatType, kCFNumberFloatType:
return self.floatValue.description

case kCFNumberFloat64Type, kCFNumberDoubleType:
return self.doubleValue.description

case kCFNumberCGFloatType:
if Int.max == Int32.max {
return self.floatValue.description
} else {
return self.doubleValue.description
}
default: fatalError("Unknown NSNumber Type")
}
}

internal func _cfNumberType() -> CFNumberType {
switch objCType.pointee {
case 0x42: return kCFNumberCharType
Expand Down
27 changes: 26 additions & 1 deletion TestFoundation/TestDecimal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ class TestDecimal: XCTestCase {
XCTAssertEqual(1, ten._length)
}

func test_NSDecimal() {
func test_NSDecimal() throws {
var nan = Decimal.nan
XCTAssertTrue(NSDecimalIsNotANumber(&nan))
var zero = Decimal()
Expand All @@ -539,6 +539,31 @@ class TestDecimal: XCTestCase {
let nsd1 = NSDecimalNumber(decimal: Decimal(2657.6))
let nsd2 = NSDecimalNumber(floatLiteral: 2657.6)
XCTAssertEqual(nsd1, nsd2)

XCTAssertEqual(NSDecimalNumber(decimal: Decimal(Int8.min)).description, Int8.min.description)
XCTAssertEqual(NSDecimalNumber(decimal: Decimal(Int8.max)).description, Int8.max.description)
XCTAssertEqual(NSDecimalNumber(decimal: Decimal(UInt8.min)).description, UInt8.min.description)
XCTAssertEqual(NSDecimalNumber(decimal: Decimal(UInt8.max)).description, UInt8.max.description)

XCTAssertEqual(NSDecimalNumber(decimal: Decimal(Int16.min)).description, Int16.min.description)
XCTAssertEqual(NSDecimalNumber(decimal: Decimal(Int16.max)).description, Int16.max.description)
XCTAssertEqual(NSDecimalNumber(decimal: Decimal(UInt16.min)).description, UInt16.min.description)
XCTAssertEqual(NSDecimalNumber(decimal: Decimal(UInt16.max)).description, UInt16.max.description)

XCTAssertEqual(NSDecimalNumber(decimal: Decimal(Int32.min)).description, Int32.min.description)
XCTAssertEqual(NSDecimalNumber(decimal: Decimal(Int32.max)).description, Int32.max.description)
XCTAssertEqual(NSDecimalNumber(decimal: Decimal(UInt32.min)).description, UInt32.min.description)
XCTAssertEqual(NSDecimalNumber(decimal: Decimal(UInt32.max)).description, UInt32.max.description)

XCTAssertEqual(NSDecimalNumber(decimal: Decimal(Int64.min)).description, Int64.min.description)
XCTAssertEqual(NSDecimalNumber(decimal: Decimal(Int64.max)).description, Int64.max.description)
XCTAssertEqual(NSDecimalNumber(decimal: Decimal(UInt64.min)).description, UInt64.min.description)
XCTAssertEqual(NSDecimalNumber(decimal: Decimal(UInt64.max)).description, UInt64.max.description)

XCTAssertEqual(try NSDecimalNumber(decimal: Decimal(string: "12.34").unwrapped()).description, "12.34")
XCTAssertEqual(try NSDecimalNumber(decimal: Decimal(string: "0.0001").unwrapped()).description, "0.0001")
XCTAssertEqual(try NSDecimalNumber(decimal: Decimal(string: "-1.0002").unwrapped()).description, "-1.0002")
XCTAssertEqual(try NSDecimalNumber(decimal: Decimal(string: "0.0").unwrapped()).description, "0")
}

func test_PositivePowers() {
Expand Down
105 changes: 92 additions & 13 deletions TestFoundation/TestNSNumber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1016,21 +1016,100 @@ class TestNSNumber : XCTestCase {


func test_description() {
let nsnumber: NSNumber = 1000
let expectedDesc = "1000"
XCTAssertEqual(nsnumber.description, expectedDesc, "expected \(expectedDesc) but received \(nsnumber.description)")
XCTAssertEqual(NSNumber(value: 1000).description, "1000")
XCTAssertEqual(NSNumber(value: 0.001).description, "0.001")

XCTAssertEqual(NSNumber(value: Int8.min).description, "-128")
XCTAssertEqual(NSNumber(value: Int8.max).description, "127")
XCTAssertEqual(NSNumber(value: Int16.min).description, "-32768")
XCTAssertEqual(NSNumber(value: Int16.max).description, "32767")
XCTAssertEqual(NSNumber(value: Int32.min).description, "-2147483648")
XCTAssertEqual(NSNumber(value: Int32.max).description, "2147483647")
XCTAssertEqual(NSNumber(value: Int64.min).description, "-9223372036854775808")
XCTAssertEqual(NSNumber(value: Int64.max).description, "9223372036854775807")

XCTAssertEqual(NSNumber(value: UInt8.min).description, "0")
XCTAssertEqual(NSNumber(value: UInt8.max).description, "255")
XCTAssertEqual(NSNumber(value: UInt16.min).description, "0")
XCTAssertEqual(NSNumber(value: UInt16.max).description, "65535")
XCTAssertEqual(NSNumber(value: UInt32.min).description, "0")
XCTAssertEqual(NSNumber(value: UInt32.max).description, "4294967295")
XCTAssertEqual(NSNumber(value: UInt64.min).description, "0")
XCTAssertEqual(NSNumber(value: UInt64.max).description, "18446744073709551615")
}

func test_descriptionWithLocale() {
let nsnumber: NSNumber = 1000
let values : Dictionary = [
Locale(identifier: "en_GB") : "1,000",
Locale(identifier: "de_DE") : "1.000",
]
for (locale, expectedDesc) in values {
let receivedDesc = nsnumber.description(withLocale: locale)
XCTAssertEqual(receivedDesc, expectedDesc, "expected \(expectedDesc) but received \(receivedDesc)")
}
// nil Locale
XCTAssertEqual(NSNumber(value: 1000).description(withLocale: nil), "1000")
XCTAssertEqual(NSNumber(value: 0.001).description(withLocale: nil), "0.001")

XCTAssertEqual(NSNumber(value: Int8.min).description(withLocale: nil), "-128")
XCTAssertEqual(NSNumber(value: Int8.max).description(withLocale: nil), "127")
XCTAssertEqual(NSNumber(value: Int16.min).description(withLocale: nil), "-32768")
XCTAssertEqual(NSNumber(value: Int16.max).description(withLocale: nil), "32767")
XCTAssertEqual(NSNumber(value: Int32.min).description(withLocale: nil), "-2147483648")
XCTAssertEqual(NSNumber(value: Int32.max).description(withLocale: nil), "2147483647")
XCTAssertEqual(NSNumber(value: Int64.min).description(withLocale: nil), "-9223372036854775808")
XCTAssertEqual(NSNumber(value: Int64.max).description(withLocale: nil), "9223372036854775807")

XCTAssertEqual(NSNumber(value: UInt8.min).description(withLocale: nil), "0")
XCTAssertEqual(NSNumber(value: UInt8.max).description(withLocale: nil), "255")
XCTAssertEqual(NSNumber(value: UInt16.min).description(withLocale: nil), "0")
XCTAssertEqual(NSNumber(value: UInt16.max).description(withLocale: nil), "65535")
XCTAssertEqual(NSNumber(value: UInt32.min).description(withLocale: nil), "0")
XCTAssertEqual(NSNumber(value: UInt32.max).description(withLocale: nil), "4294967295")
XCTAssertEqual(NSNumber(value: UInt64.min).description(withLocale: nil), "0")
XCTAssertEqual(NSNumber(value: UInt64.max).description(withLocale: nil), "18446744073709551615")

// en_GB Locale
XCTAssertEqual(NSNumber(value: 1000).description(withLocale: Locale(identifier: "en_GB")), "1,000")
XCTAssertEqual(NSNumber(value: 0.001).description(withLocale: Locale(identifier: "en_GB")), "0.001")

XCTAssertEqual(NSNumber(value: Int8.min).description(withLocale: Locale(identifier: "en_GB")), "-128")
XCTAssertEqual(NSNumber(value: Int8.max).description(withLocale: Locale(identifier: "en_GB")), "127")
XCTAssertEqual(NSNumber(value: Int16.min).description(withLocale: Locale(identifier: "en_GB")), "-32,768")
XCTAssertEqual(NSNumber(value: Int16.max).description(withLocale: Locale(identifier: "en_GB")), "32,767")
XCTAssertEqual(NSNumber(value: Int32.min).description(withLocale: Locale(identifier: "en_GB")), "-2,147,483,648")
XCTAssertEqual(NSNumber(value: Int32.max).description(withLocale: Locale(identifier: "en_GB")), "2,147,483,647")
XCTAssertEqual(NSNumber(value: Int64.min).description(withLocale: Locale(identifier: "en_GB")), "-9,223,372,036,854,775,808")
XCTAssertEqual(NSNumber(value: Int64.max).description(withLocale: Locale(identifier: "en_GB")), "9,223,372,036,854,775,807")

XCTAssertEqual(NSNumber(value: UInt8.min).description(withLocale: Locale(identifier: "en_GB")), "0")
XCTAssertEqual(NSNumber(value: UInt8.max).description(withLocale: Locale(identifier: "en_GB")), "255")
XCTAssertEqual(NSNumber(value: UInt16.min).description(withLocale: Locale(identifier: "en_GB")), "0")
XCTAssertEqual(NSNumber(value: UInt16.max).description(withLocale: Locale(identifier: "en_GB")), "65,535")
XCTAssertEqual(NSNumber(value: UInt32.min).description(withLocale: Locale(identifier: "en_GB")), "0")
XCTAssertEqual(NSNumber(value: UInt32.max).description(withLocale: Locale(identifier: "en_GB")), "4,294,967,295")
XCTAssertEqual(NSNumber(value: UInt64.min).description(withLocale: Locale(identifier: "en_GB")), "0")

// This is the correct value but currently buggy and the locale is not used
// XCTAssertEqual(NSNumber(value: UInt64.max).description(withLocale: Locale(identifier: "en_GB")), "18,446,744,073,709,551,615")
XCTAssertEqual(NSNumber(value: UInt64.max).description(withLocale: Locale(identifier: "en_GB")), "18446744073709551615")

// de_DE Locale
XCTAssertEqual(NSNumber(value: 1000).description(withLocale: Locale(identifier: "de_DE")), "1.000")
XCTAssertEqual(NSNumber(value: 0.001).description(withLocale: Locale(identifier: "de_DE")), "0,001")

XCTAssertEqual(NSNumber(value: Int8.min).description(withLocale: Locale(identifier: "de_DE")), "-128")
XCTAssertEqual(NSNumber(value: Int8.max).description(withLocale: Locale(identifier: "de_DE")), "127")
XCTAssertEqual(NSNumber(value: Int16.min).description(withLocale: Locale(identifier: "de_DE")), "-32.768")
XCTAssertEqual(NSNumber(value: Int16.max).description(withLocale: Locale(identifier: "de_DE")), "32.767")
XCTAssertEqual(NSNumber(value: Int32.min).description(withLocale: Locale(identifier: "de_DE")), "-2.147.483.648")
XCTAssertEqual(NSNumber(value: Int32.max).description(withLocale: Locale(identifier: "de_DE")), "2.147.483.647")
XCTAssertEqual(NSNumber(value: Int64.min).description(withLocale: Locale(identifier: "de_DE")), "-9.223.372.036.854.775.808")
XCTAssertEqual(NSNumber(value: Int64.max).description(withLocale: Locale(identifier: "de_DE")), "9.223.372.036.854.775.807")

XCTAssertEqual(NSNumber(value: UInt8.min).description(withLocale: Locale(identifier: "de_DE")), "0")
XCTAssertEqual(NSNumber(value: UInt8.max).description(withLocale: Locale(identifier: "de_DE")), "255")
XCTAssertEqual(NSNumber(value: UInt16.min).description(withLocale: Locale(identifier: "de_DE")), "0")
XCTAssertEqual(NSNumber(value: UInt16.max).description(withLocale: Locale(identifier: "de_DE")), "65.535")
XCTAssertEqual(NSNumber(value: UInt32.min).description(withLocale: Locale(identifier: "de_DE")), "0")
XCTAssertEqual(NSNumber(value: UInt32.max).description(withLocale: Locale(identifier: "de_DE")), "4.294.967.295")
XCTAssertEqual(NSNumber(value: UInt64.min).description(withLocale: Locale(identifier: "de_DE")), "0")

// This is the correct value but currently buggy and the locale is not used
//XCTAssertEqual(NSNumber(value: UInt64.max).description(withLocale: Locale(identifier: "de_DE")), "18.446.744.073.709.551.615")
XCTAssertEqual(NSNumber(value: UInt64.max).description(withLocale: Locale(identifier: "de_DE")), "18446744073709551615")
}

func test_objCType() {
Expand Down
6 changes: 2 additions & 4 deletions TestFoundation/TestNSString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -832,19 +832,17 @@ class TestNSString: LoopbackServerTest {
XCTAssertEqual(string, "Default value is 1000 (42.0)")
}

#if false // these two tests expose bugs in icu4c's localization on some linux builds (disable until we can get a uniform fix for this)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this no longer applicable after this patch?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think enabling the __CFStringFormatLocalizedNumber makes it work on Linux now. I tested it on 14.04 and it seemed ok

withVaList(argument) {
pointer in
let string = NSString(format: "en_GB value is %d (%.1f)", locale: Locale.init(localeIdentifier: "en_GB"), arguments: pointer)
let string = NSString(format: "en_GB value is %d (%.1f)", locale: Locale.init(identifier: "en_GB") as AnyObject, arguments: pointer)
XCTAssertEqual(string, "en_GB value is 1,000 (42.0)")
}

withVaList(argument) {
pointer in
let string = NSString(format: "de_DE value is %d (%.1f)", locale: Locale.init(localeIdentifier: "de_DE"), arguments: pointer)
let string = NSString(format: "de_DE value is %d (%.1f)", locale: Locale.init(identifier: "de_DE") as AnyObject, arguments: pointer)
XCTAssertEqual(string, "de_DE value is 1.000 (42,0)")
}
#endif

withVaList(argument) {
pointer in
Expand Down