Formatting numbers in Swift.
Bài đăng này đã không được cập nhật trong 4 năm
-
Number
là mộttype
quan trọng mà chúng ta gặp rất nhiều trong công việc lập trình hàng ngày. Chúng ta bứt gặpnumber
ở khắp mọi nơi từ tính toán kích thước, bố cục choUI
cho đếnconditional
của cácevent
. -
Làm việc với
number
là một trong công việc chính mà máy tính được tạo ra nhưng đôi khi để con người có thể giao tiếp được với máy tính thìnumber
phải được chuyển về cácformat
mà con người có thể đọc hiểu để đảm bảo tính chính xác cũng như chặt chẽ theo yêu cầu.
1/ Solving the decimal problem:
- Nếu yêu cầu đơn giản chúng ta có thể sử dụng
string literal
để khởi tạonumber
từ dạngString
như sau:
let a = String(42) // "42"
let b = String(3.14) // "3.14"
let c = "\(42), \(3.14)" // "42, 3.14"
-
Phương pháp hoạt động tốt và dễ tiếp cận và cho phép chúng ta hoàn toàn kiểm soát các giá trị của
number
, nhưng để tăng tính cơ động trong mục đích sử dụng chúng ta có thể cần các phương thức cũng như định dạng khác khi làm việc với các giá trịnumber
thay đổi liên tục. -
Để lấy ví dụ chúng ta sẽ triển khai
type
Metric
cho phép chúng ta liên kếttype
Double
với cácname
,value
,description
:
struct Metric: Codable {
var name: String
var value: Double
}
extension Metric: CustomStringConvertible {
var description: String {
"\(name): \(value)"
}
}
- Để cho
number
dạngDouble
có thể dễ đọc hơn ta có thể chuyển nó từDouble
sang dạngString
bằng cách có thể tùy chỉnh vị trí thập phân để có thể đảm bảooutput
ra luôn nhất quán:
extension Metric: CustomStringConvertible {
var description: String {
let formattedValue = String(format: "%.2f", value)
return "\(name): \(formattedValue)"
}
}
-
Chúng ta gặp vấn đề ở đây khi nếu
number
là dạng số nguyên thìoutput
cũng vẫn sẽ hiển thị dấu thập phân, ví dụ nhưnumber
42
sẽ cho raoutput
là42.00
. -
Cách giải quyết vấn đề này là chúng ta sẽ lọc và bỏ bớt các
number
sau dấu thập phân nếu chúng là0
như sau:
extension Metric: CustomStringConvertible {
var description: String {
var formattedValue = String(format: "%.2f", value)
while formattedValue.last == "0" {
formattedValue.removeLast()
}
if formattedValue.last == "." {
formattedValue.removeLast()
}
return "\(name): \(formattedValue)"
}
}
- Đoạn
code
trên hoạt động có vẻ ổn nhưng chưa gọn gàng lắm và đây không phải là một hướng tiếp cận cho tất cả trường hợp sử dụng, vì trong các ngôn ngữ khác nhau sẽ có các phương thức biểu diễnnumber
khác nhau và chúng ta không thể cứ xử lý các trường hợp cá biệt cho từng ngôn ngữ được.
2/ Using NumberFormatter:
-
Sử dụng
NumberFormatter
trongFoundation
để giải quyết vấn đề trên củanumber
sau dấu thập phân nhưDateFormatter
định dạng các giá trịDate
là một lựa chọn tốt hơn vìNumberFormatter
đã cung cấp cho chúng ta đầy đủ các công cụ cần thiết để chúng ta làm việc vớinumber
. -
Với
NumberFormatter
chúng ta có thể làm việc với từngnumber
có thập phân hay không có thập phân và hiển thịnumber
chính xác đến từng số sau dấu thập phân mà chúng ta mong muốn như42
,42.1
,42.22
:
extension Metric: CustomStringConvertible {
var description: String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
let number = NSNumber(value: value)
let formattedValue = formatter.string(from: number)!
return "\(name): \(formattedValue)"
}
}
-
Một lợi ích nữa chúng ta nhận được khi sử dụng
NumberFormatter
là nó sẽ tự động sử dụngLocale
mà chúng ta đang sử dụng để định dạng. Ví dụ như ở một số quốc gianumber
69969.69
sẽ được định dạng thành69 969,69
thay vì69,969.69
. Các trường hợp phức tạp trên chúng ta đều có thể xử lý dễ dàng và tự động. -
Không phải trong tất cả các trường hợp chúng ta đều có thể sử dụng
Locale
, chúng ta cần tùy chỉnh thêmdecimalSeparator
vàgroupingSeparator
nhưu sau:
extension Metric: CustomStringConvertible {
var description: String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
formatter.decimalSeparator = "."
formatter.groupingSeparator = ""
...
}
}
- Trong trường hợp này chúng ta muốn các định dạng
formatting
của chúng ta phải tuân theo từng khu vực đang sử dụng. Chúng ta cần tạo thêmstatic
property
cho chúng ta có thể sử dụng lại cácMetric
value
:
extension Metric: CustomStringConvertible {
private static var valueFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
return formatter
}()
var formattedValue: String {
let number = NSNumber(value: value)
return Self.valueFormatter.string(from: number)!
}
var description: String {
"\(name): \(formattedValue)"
}
}
3/ Domain-specific numbers:
-
Tùy thuộc vào kiểu
app
mà chúng ta đang làm việc cùng mà chúng ta sẽ có cơ hội đối diện vớidomain-specific
. Trong trường hợp đó chúng ta sẽ cần phải có thêm cácdescription
củanumber
hơn là chỉ vớivalue
của nó: -
Chúng ta làm việc với
shopping app
và chúng ta sử dụngDouble
để biểu diễnPrice
struct
để mô tả giá sản phẩm:
struct Product: Codable {
var name: String
var price: Price
...
}
struct Price: Codable {
var amount: Double
var currency: Currency
}
enum Currency: String, Codable {
case eur
case usd
case sek
case pln
...
}
-
Câu hỏi được đặt ra ở đây là làm thế nào chúng ta có thể định dạng
instance
Price
cho từnguser
ở từng quốc gia khác nhau vớiLocale
khác nhau: -
Đây là một trường hợp mà
NumberFormatter
tỏ ra vô cùng hữu dụng bao gồm cả việcfull-support
choLocale
cũng như việc có thể tùy chỉnhnumberStyle
vàcurrency
như sauL
extension Price: CustomStringConvertible {
var description: String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = currency.rawValue
formatter.maximumFractionDigits = 2
let number = NSNumber(value: amount)
return formatter.string(from: number)!
}
}
- Kết quả chúng ta nhận được ở đây sẽ là định giá cho từng quốc gia sử dụng
Locale
khác nhau, ví dụ nhưprice
3.14
ở Sweden sử dụng đơn vị tiền tệ làSEK
sẽ được hiển thị như sau- Sweden:
3,14 kr
- Spain:
3.14 SEK
- US:
SEK 3.14
- France:
SEK 3,14
- Sweden:
- Bên cạnh
price
chúng ta thường gặp trường hợp các giá trịnumber
hiển thị theo các thông số đo lường. Ví dụ như chúng ta muốn tập trung vào vận tốc cao nhất đạt được của một chiếc xe nhưtopSpeed
:
struct Vehicle {
var name: String
var price: Price
var topSpeed: Double
...
}
- Trong trường hợp này thì để đo lường một cách chính xác
topSpeed
thìDouble
không phải là lựa chọn tối ưu mà thay vào đó chúng ta có thể sử dụng luônMeasurement
với mộtphantom type
làUnitSpeed
:
struct Vehicle {
var name: String
var price: Price
var topSpeed: Measurement<UnitSpeed>
...
}
- Trong khi khởi tạo
Vehicle
instance
chúng ta có thể sẽ phải chỉ định rõ đơn vị đo lường chotopSpeed
để tránh sự mơ hồ. DoMeasurement
có sẵnformatter
nên chúng ta có thể dễ dàng xử lý vấn đề này:
extension Vehicle {
var formattedTopSpeed: String {
let formatter = MeasurementFormatter()
return formatter.string(from: topSpeed)
}
}
- Chúng ta cần ghi nhớ rằng khi sử dựng
Measurement
chúng ta sẽ đồng thời sử dụngencoded
vàdecoded
nên chúng ta sẽ cầnCodeable
đểMeasurement
để đảm bảo khi giá trị chúng ta có thể không nằm trongJSON
mà chúng ta có thểdownload
từapp server
. Trường hợp này chúng ta sẽ cần thêm các đơn vị đo lường đã được thống nhất để quá trìnhdecoded
hoạt động tốt:
extension Vehicle: Codable {
private enum CodingKeys: CodingKey {
case name, price, topSpeed, ...
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Decoding all other properties
...
topSpeed = try Measurement(
value: container.decode(Double.self, forKey: .topSpeed),
unit: .kilometersPerHour
)
}
// Encoding implementation
...
}
- Chúng ta bây giờ có thể sử dụng các
property wrapper
để đóng gói sự chuyển đổi giữaDouble
vàMeasurement
với mộttype
duy nhất:
@propertyWrapper
struct KilometersPerHour {
var wrappedValue: Measurement<UnitSpeed>
}
extension KilometersPerHour: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let rawValue = try container.decode(Double.self)
wrappedValue = Measurement(
value: rawValue,
unit: .kilometersPerHour
)
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue.value)
}
}
- Tốn bao công giờ chúng ta thu hoặc lợi ích naofm chúng ra giờ có thể đánh dấu
topSpeed
property
với@KilometersPerHour
:
struct Vehicle: Codable {
var name: String
var price: Price
@KilometersPerHour var topSpeed: Measurement<UnitSpeed>
...
}
- Chúng ta giờ đã đảm bảo cho việc sử dụng một cách an toàn và hiệu quả cho
Measurement
từ triển khai trên phục vụ cho việc định dạng và chuyển đổi thích họp cho các tính năng chúng ta cần khi phát triểnapp
khiencoding
vàdecoding
data model
.
All rights reserved