Formatting numbers in Swift.
Bài đăng này đã không được cập nhật trong 4 năm
- 
Numberlà mộttypequan 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 choUIcho đếnconditionalcủa cácevent.
- 
Làm việc với numberlà 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ìnumberphải được chuyển về cácformatmà 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ạonumbertừ dạngStringnhư 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ịnumberthay đổi liên tục.
- 
Để lấy ví dụ chúng ta sẽ triển khai typeMetriccho phép chúng ta liên kếttypeDoublevới cácname,value,description:
struct Metric: Codable {
    var name: String
    var value: Double
}
extension Metric: CustomStringConvertible {
    var description: String {
        "\(name): \(value)"
    }
}
- Để cho numberdạngDoublecó thể dễ đọc hơn ta có thể chuyển nó từDoublesang dạngStringbằng cách có thể tùy chỉnh vị trí thập phân để có thể đảm bảooutputra 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 numberlà dạng số nguyên thìoutputcũng vẫn sẽ hiển thị dấu thập phân, ví dụ nhưnumber42sẽ cho raoutputlà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 numbersau dấu thập phân nếu chúng là0như 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 codetrê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ễnnumberkhá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 NumberFormattertrongFoundationđể giải quyết vấn đề trên củanumbersau dấu thập phân nhưDateFormatterđịnh dạng các giá trịDatelà 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 NumberFormatterchúng ta có thể làm việc với từngnumbercó thập phân hay không có thập phân và hiển thịnumberchí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 NumberFormatterlà nó sẽ tự động sử dụngLocalemà chúng ta đang sử dụng để định dạng. Ví dụ như ở một số quốc gianumber69969.69sẽ được định dạng thành69 969,69thay 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êmdecimalSeparatorvàgroupingSeparatornhư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 formattingcủ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êmstaticpropertycho chúng ta có thể sử dụng lại cácMetricvalue:
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 appmà 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ácdescriptioncủanumberhơn là chỉ vớivaluecủa nó:
- 
Chúng ta làm việc với shopping appvà chúng ta sử dụngDoubleđể biểu diễnPricestructđể 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 instancePricecho từnguserở từng quốc gia khác nhau vớiLocalekhác nhau:
- 
Đây là một trường hợp mà NumberFormattertỏ ra vô cùng hữu dụng bao gồm cả việcfull-supportchoLocalecũng như việc có thể tùy chỉnhnumberStylevàcurrencynhư 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 Localekhác nhau, ví dụ nhưprice3.14ở Sweden sử dụng đơn vị tiền tệ làSEKsẽ đượ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 pricechúng ta thường gặp trường hợp các giá trịnumberhiể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 topSpeedthìDoublekhông phải là lựa chọn tối ưu mà thay vào đó chúng ta có thể sử dụng luônMeasurementvới mộtphantom typelàUnitSpeed:
struct Vehicle {
    var name: String
    var price: Price
    var topSpeed: Measurement<UnitSpeed>
    ...
}
- Trong khi khởi tạo Vehicleinstancechúng ta có thể sẽ phải chỉ định rõ đơn vị đo lường chotopSpeedđể tránh sự mơ hồ. DoMeasurementcó sẵnformatternê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 Measurementchúng ta sẽ đồng thời sử dụngencodedvàdecodednên chúng ta sẽ cầnCodeableđểMeasurementđể đảm bảo khi giá trị chúng ta có thể không nằm trongJSONmà chúng ta có thểdownloadtừ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ìnhdecodedhoạ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ữaDoublevàMeasurementvới mộttypeduy 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 topSpeedpropertyvớ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 Measurementtừ 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ểnappkhiencodingvàdecodingdata model.
All rights reserved
 
  
 