목차
- input과 output의 Locale 설정을 분리합니다.
- Locale 설정을 생략하지 못하도록 해야합니다.
- 에러를 리턴할 수 있어야 합니다.
- 지정 국가의 숫자로 이루어진 경우
- 🇺🇸 en_US ) 123
- 🇺🇸 en_US ) 12000
- 🇳🇵 ne ) १२३
- 1이면서 소수점 구분 문자(decimal separator)가 있는 경우
- 🇺🇸 en_US ) 1.23
- 🇻🇳 vi ) 1,23
- 1이면서 그룹 구분 문자(grouping separator)가 있는 경우
- 🇺🇸 en_US ) 1,200
- 🇻🇳 vi ) 1.200
- 1이면서 소숫점 구분 문자, 천단위 구분 문자가 모두 있는 경우
- 🇺🇸 en_US ) 1,203.3
- 🇻🇳 vi ) 1.203,3
빈문자열("")
,점(".")
은 0으로 처리- first 또는 last character가 decimalSeparator면 0 처리
- 🇺🇸 en_US ) ".134" == 0.134
- 🇺🇸 en_US ) "123." == 123.0
- 음수일 경우
- 특수 숫자가 섞여있을 경우
- 123१२३۱۲۳
enum LocalizedNumberFormatterError: Error {
case invaildCharacters
case moreThanOneDecimalSeparator
case reverseSeparator
case notConvertedNSNumberToLocalizedString
case unknown
var message: String {
switch self {
case .invaildCharacters:
return "올바르지 않은 문자가 포함되어있습니다."
case .moreThanOneDecimalSeparator:
return "소숫점 기호가 한 개 이상입니다."
case .reverseSeparator:
return "그룹 구분 기호와 소숫점 기호가 반대로 되어있습니다."
case .notConvertedNSNumberToLocalizedString:
return "현지화 문자로 변환 되지 않는 숫자입니다."
case .unknown:
return "알 수 없는 에러입니다."
}
}
}
protocol LocalizedNumberFormatterDataSource {
var fromLocale: Locale { get }
var toLocale: Locale { get }
// input String -> NSNumber
func number(from value: String) throws -> NSNumber
// input string -> output localized number string
func localizedNumberString(from value: String, style: NumberFormatter.Style) throws -> String
}
class LocalizedNumberFormatter: LocalizedNumberFormatterDataSource {
typealias ConvertedError = LocalizedNumberFormatterError
var fromLocale: Locale
var toLocale: Locale
private let log: Logger = Logger(logPlace: LocalizedNumberFormatter.self)
init(from: Locale, to: Locale) {
self.fromLocale = from
self.toLocale = to
}
}
extension LocalizedNumberFormatter {
func number(from value: String) throws -> NSNumber {
let numberFormatter: NumberFormatter = NumberFormatter()
numberFormatter.locale = fromLocale
//
let splitValues: (integer: String, decimal: String) = try splitToDecimal(locale: fromLocale, value: value)
let integerValue: String = splitValues.integer.isEmpty
? "0" // ".12" -> "0.12"
: splitValues.integer.replacingOccurrences(of: numberFormatter.groupingSeparator, with: "")
let decimalValue: String = splitValues.decimal.isEmpty
? "0" // "1." -> "1.0"
: splitValues.decimal
guard false == (integerValue.contains(numberFormatter.decimalSeparator))
&& false == (decimalValue.contains(numberFormatter.groupingSeparator)) else {
log.error(message: " - value : \(value)\n - result : \(ConvertedError.reverseSeparator)")
throw ConvertedError.reverseSeparator
}
guard let result = numberFormatter.number(from: integerValue + numberFormatter.decimalSeparator + decimalValue) else {
log.error(message: " - value : \(value)\n - result : \(ConvertedError.invaildCharacters)")
throw ConvertedError.invaildCharacters
}
log.info(message: " - value : \(value)\n - result : \(result)")
return result
}
private func splitToDecimal(locale: Locale, value: String) throws -> (integer: String, decimal: String) {
guard false == value.isEmpty && "." != value else {
return (integer: "0", decimal: "0")
}
let numberFormatter: NumberFormatter = NumberFormatter()
numberFormatter.locale = locale
let splitedValues: [String] = value.components(separatedBy: numberFormatter.decimalSeparator)
guard splitedValues.count <= 2 else { // DecimalSeparator 두개 이상이면 에러
log.error(message: " - value: \(value)\n - error: \(ConvertedError.moreThanOneDecimalSeparator)")
throw ConvertedError.moreThanOneDecimalSeparator
}
if 1 == splitedValues.count {
let result = (integer: splitedValues.first ?? "", decimal: "")
return result
}
guard 2 == splitedValues.count else {
// 유효하지 않은 포맷
log.error(message: " - value: \(value)\n - error: \(ConvertedError.unknown)")
throw ConvertedError.unknown
}
let integerValue = splitedValues.first!.replacingOccurrences(of: numberFormatter.groupingSeparator, with: "")
let decimalValue = splitedValues.last!
return (integer: integerValue, decimal: decimalValue)
}
}
extension LocalizedNumberFormatter {
func localizedNumberString(from value: String, style: NumberFormatter.Style) throws -> String {
do {
let number: NSNumber = try self.number(from: value)
let numberFormatter: NumberFormatter = NumberFormatter()
numberFormatter.locale = toLocale
numberFormatter.numberStyle = style
guard let localizedString = numberFormatter.string(from: number) else {
log.error(message: " - value : \(value)\n - error : \(ConvertedError.notConvertedNSNumberToLocalizedString)")
throw ConvertedError.notConvertedNSNumberToLocalizedString
}
log.info(message: " - value : \(value)\n - result : \(localizedString)")
return localizedString
} catch {
throw error
}
}
}
grouping separator
가 포함되어있는 경우, NumberFormatter
의 open func number(from string: String) -> NSNumber?
가 nil
이 됩니다.<예시>
let value = "-1,000.023"
let numberFormatter: NumberFormatter = NumberFormatter()
numberFormatter.locale = Locale(identifier: "en_US")
(lldb) po numberFormatter.number(from: value)
nil
func splitToDecimal(locale: Locale, value: String) throws -> (integer: String, decimal: String)
import XCTest
@testable import LocalizedNumberField
class LocalizedNumberFormatterTest: XCTestCase {
private let log = Logger(logPlace: LocalizedNumberFormatterTest.self)
private let numberFormatter: NumberFormatter = NumberFormatter()
private let locale_enUS = Locale.en_US
// "."은 0.0 으로 처리할 것
let vaildAmounts = [
"1234",
"1,234",
"1.2",
"1,200.13",
// 0으로 처리
"",
".",
//first or last가 decimalSeparator면 0 처리
".134", // 0.134
"123.", // 123.0
// 음수
"-101010",
"-1010.10",
"-1,000.023",
// 특수 숫자 섞일 경우
"123१२३۱۲۳",
"123१२३.۱۲۳",
"123,१२३.۱۲۳"
]
let invaildAmounts = [
"0.0.1",
"10️⃣0️⃣",
"loo",
"lzoo",
"123.456,789"
]
func testValueToNumber() throws {
for vaild in vaildAmounts {
let formatter = LocalizedNumberFormatter(from: locale_enUS, to: locale_enUS)
XCTAssertNoThrow(try formatter.number(from: vaild))
}
for invaild in invaildAmounts {
let formatter = LocalizedNumberFormatter(from: locale_enUS, to: locale_enUS)
XCTAssertThrowsError(try formatter.number(from: invaild))
}
}
let vaildAmountLocalizedStrings_enUS: [String: String] = [
"1234": "1,234",
"1,234": "1,234",
"1.2": "1.2",
"1,200.13": "1,200.13",
// 0으로 처리
"": "0",
".": "0",
//first or last가 decimalSeparator면 0 처리
".134": "0.134",
"123.": "123",
// 음수
"-101010": "-101,010",
"-1010.10": "-1,010.1",
"-1,000.023": "-1,000.023",
// 특수 숫자 섞일 경우
"123१२३۱۲۳": "123,123,123",
"123१२३.۱۲۳": "123,123.123",
"123,१२३.۱۲۳": "123,123.123"
]
func testToLocalizedNumberString() throws {
for vaildAmount in vaildAmounts {
let formatter = LocalizedNumberFormatter(from: .current, to: .current)
let localizedString = try formatter.localizedNumberString(from: vaildAmount, style: .decimal)
XCTAssertEqual(vaildAmountLocalizedStrings_enUS[vaildAmount] ?? "", localizedString)
}
for invaildAmount in invaildAmounts {
let formatter = LocalizedNumberFormatter(from: .current, to: .current)
XCTAssertThrowsError(try formatter.localizedNumberString(from: invaildAmount, style: .decimal))
}
}
func toNumber(value: String) throws -> NSNumber {
numberFormatter.locale = locale_enUS
let splitValues: (integer: String, decimal: String) = try splitToDecimal(to: locale_enUS, value: value)
let integerValue: String = splitValues.integer.isEmpty
? "0" // ".12" -> "0.12"
: splitValues.integer.replacingOccurrences(of: numberFormatter.groupingSeparator, with: "")
let decimalValue: String = splitValues.decimal.isEmpty
? "0" // "1." -> "1.0"
: splitValues.decimal
guard false == (integerValue.contains(numberFormatter.decimalSeparator))
&& false == (decimalValue.contains(numberFormatter.groupingSeparator)) else {
log.error(message: " value : \(value)\n - result : \(LocalizedNumberFormatterError.reverseSeparator)")
throw LocalizedNumberFormatterError.reverseSeparator
}
guard let result = numberFormatter.number(from: integerValue + numberFormatter.decimalSeparator + decimalValue) else {
log.error(message: " value : \(value)\n - result : \(LocalizedNumberFormatterError.invaildCharacters)")
throw LocalizedNumberFormatterError.invaildCharacters
}
log.info(message: " value : \(value)\n - result : \(result)")
return result
}
private func splitToDecimal(to locale: Locale, value: String) throws -> (integer: String, decimal: String) {
guard false == value.isEmpty && "." != value else {
return (integer: "0", decimal: "0")
}
let numberFormatter: NumberFormatter = NumberFormatter()
numberFormatter.locale = locale
let splitedValues: [String] = value.components(separatedBy: numberFormatter.decimalSeparator)
guard splitedValues.count <= 2 else { // DecimalSeparator 두개 이상이면 에러
log.error(message: " - value: \(value)\n - error: \(LocalizedNumberFormatterError.moreThanOneDecimalSeparator)")
throw LocalizedNumberFormatterError.moreThanOneDecimalSeparator
}
if 1 == splitedValues.count {
let result = (integer: splitedValues.first ?? "", decimal: "")
return result
}
guard 2 == splitedValues.count else {
// 유효하지 않은 포맷
log.error(message: " - value: \(value)\n - error: \(LocalizedNumberFormatterError.unknown)")
throw LocalizedNumberFormatterError.unknown
}
let integerValue = splitedValues.first!.replacingOccurrences(of: numberFormatter.groupingSeparator, with: "")
let decimalValue = splitedValues.last!
return (integer: integerValue, decimal: decimalValue)
}
}
이렇게 LocalizedNumberFomatter
가 완성되었습니다.
다음 편에서는 LocalizedNumberFomatter
이용한 예제에 대해 포스팅하겠습니다.