목차
2편까지 보았을 때, '그럼 NumberFormatter 쓰면 되는 것 아닌가?' 라고 생각할 수 있지만
사용자의 환경과 서비스 스펙에 따라 많은 변수와 제약사항이 있을 수 있습니다.
단적인 예로 Number String을 받는 Textfield에는
다양한 특수숫자 혹은 separator가 들어올 수 있고, (1편 - 다양한 숫자와 소숫점, 천단위 부분 참고)
third party keyboard를 써서 시스템 키보드와는 다른 방식으로 input 될 수 있고,
사용자가 어디선가 긁어온 텍스트를 복사/붙여넣기 할 수도 있습니다.
많은 이유로 Input 데이터는 올바른 포맷일 것이라는 보장이 없습니다.
이는 여러가지 버그를 만듭니다. 변환이 불가할 수도 있고, 아예 잘못된 값으로 변환이 될 수도 있습니다.
때문에 NumberFormatter가 숫자 텍스트를 올바르게 NSNumber로 변환해주는 조건을 알아내고,
이를 토대로 제약사항을 보완할 수 있는 기능을 개발하고자 합니다.
thousandSeparator
MacOS에서만 제공이 되므로, 라이브러리에서는 에러처리하지 않겠음.Number | en_US | ne | ur_IN | fr_FR | |
---|---|---|---|---|---|
only number | 123 | 123 | १२३ | ۱۲۳ | 123 |
decimal separator | 123.4 | 123.4 | १२३.४ | ۱۲۳٫۴ | 123,4 |
grouping separator | 123456789 | 123,456,789 | १२३,४५६,७८९ | ۱۲۳٬۴۵۶٬۷۸۹ | 123 456 789 |
all | 123456789.123 | 123,456,789.123 | १२३,४५६,७८९.१२३ | ۱۲۳٬۴۵۶٬۷۸۹٫۱۲۳ | 123 456 789,123 |
올바른 숫자 포맷이긴 하나,
NumberFormatter에 Locale로 설정한 국가와 다른 국가의 포맷일 경우,
어디까지 보장이 되나 테스트해봤습니다.
예를 들어, numberFormatter.locale = Locale(identifier: "en_US")
로 설정해놓고
Urdu (India)
의 ۱۲۳٬۴۵۶٬۷۸۹٫۱۲۳
가 잘 변환이 되는지 테스트 합니다.
선정 기준은 숫자 표기가 다르거나 grouping/decimal separator가 다른 것들입니다.
< 테스트 코드 >
func testMatchingDiffrentIdentifier() throws {
let numbers_en: [String] = [
"123",
"123.4",
"123,456,789",
"123,456,789.123"
]
let numbers_ne: [String] = [
"१२३",
"१२३.४",
"१२३,४५६,७८९",
"१२३,४५६,७८९.१२३"
]
let numbers_ur_IN: [String] = [
"۱۲۳",
"۱۲۳٫۴",
"۱۲۳٬۴۵۶٬۷۸۹",
"۱۲۳٬۴۵۶٬۷۸۹٫۱۲۳"
]
let numbers_fr_FR: [String] = [
"123",
"123,4",
"123 456 789",
"123 456 789,123"
]
let identifiers: [String] = ["en_US", "ne", "ur_IN", "fr_FR"]
let numbersDictionary: [String: [String]] = [
"en_US": numbers_en,
"ne": numbers_ne,
"ur_IN": numbers_ur_IN,
"fr_FR": numbers_fr_FR
]
for identifier in identifiers {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.locale = Locale(identifier: identifier)
for (identifier_compare, numbers_compare) in numbersDictionary {
// identifier가 다른 NumberString 테스트
guard identifier != identifier_compare else { continue }
let originNumbers = numbersDictionary[identifier]!
let compareNumbers = numbers_compare
for index in 0...originNumbers.count - 1 {
let origin = originNumbers[index]
let compare = compareNumbers[index]
let currentOriginNumber = numberFormatter.number(from: origin)
let currentCompareNumber = numberFormatter.number(from: compare)
guard nil != currentCompareNumber else {
// 🔥 String -> NSNumber로 변환 실패 시 로그
log.error(
message:
debugMessage(
originIdentifier: identifier,
compareIdentifier: identifier_compare,
origin: origin,
compare: compare,
num_origin: currentOriginNumber,
num_compare: currentCompareNumber
)
)
continue
}
let localizedNumberString_origin = numberFormatter.string(from: currentOriginNumber!)
let localizedNumberString_compare = numberFormatter.string(from: currentCompareNumber!)
XCTAssertEqual(localizedNumberString_origin, localizedNumberString_compare)
log.info(
message:
debugMessage(
originIdentifier: identifier,
compareIdentifier: identifier_compare,
origin: origin,
compare: compare,
num_origin: currentOriginNumber,
num_compare: currentCompareNumber,
numberString_origin: localizedNumberString_origin,
numberString_compare: localizedNumberString_compare
)
)
}
}
}
}
groupingSeparator
또는 decimalSeparator
가 포함되어있는 경우는 올바른 변환을 보장할 수 없다.let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.locale = Locale(identifier: "en_US")
// all number
(lldb) po numberFormatter.number(from: "123१२३۱۲۳")
▿ Optional<NSNumber>
- some : 123123123
// decimalSeparator
(lldb) po numberFormatter.number(from: "123.१२३۱۲۳")
▿ Optional<NSNumber>
- some : 123.123123
// groupingSeparator
(lldb) po numberFormatter.number(from: "123,१२३,۱۲۳")
nil
// groupingSeparator, decimalSeparator
(lldb) po numberFormatter.number(from: "123,१२३,۱۲۳.1")
nil
// decimalSeparator이 여러 개 일 때
(lldb) po numberFormatter.number(from: "123.१२३.۱۲۳.1")
nil
// groupingSeparator, decimalSeparator 순서가 바뀌었을 때
(lldb) po numberFormatter.number(from: "123.१२३.۱۲۳,1")
nil
우리는 input이 어떤 Locale의 포맷일지 알 수 없습니다.
그러나 NumberFormatter는 output에 대한 Locale 만을 설정합니다.
문제 상황 재현
// 베트남은 소숫점을 ","으로 표기
let vietnameseNumberString: String ="1,234" // Double(1.234)
let numberFormatter: NumberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.locale = Locale(identifier: "en_US") // 미국은 천단위를 ","로 표기
let currentCompareNumber = numberFormatter.number(from: vietnameseNumberString)
print(currentCompareNumber) // Int(1234)
let localizedNumberString_origin = numberFormatter.string(from: currentCompareNumber!)
print(localizedNumberString_origin) // String("1,234")
Double(1.234)
인데, 변환된 output 값은 Int(1234)
가 되었습니다..
) / decimalSeparator(,
),
) / decimalSeparator(.
)이렇게 NumberFormatter에 설정된 locale과 다른 숫자 포맷이 들어왔을 경우,
전혀 다른 숫자로 변환될 수 있는 위험이 있습니다.
때문에, input 데이터에 대한 Locale이 필수적으로 설정되어야 한다는 것입니다.
let numberFormatter: NumberFormatter = NumberFormatter()
// numberFormatter.locale = .current (생략 가능)
.current
가 설정 됩니다.identifier
또는 current
설정을 생략할 수 없도록 합니다.😇 이러한 보완점들을 바탕으로 LocalizedNumberFormatter와 LocaliedNumberField 라이브러리를 개발해보겠습니다....!
* https://github.com/baecheese/LocalizedNumberField