| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- //
- // QuantityFormatter.swift
- // LoopKit
- //
- // Copyright © 2018 LoopKit Authors. All rights reserved.
- //
- import Foundation
- import HealthKit
- /// Formats unit quantities as localized strings
- open class QuantityFormatter {
- public init() {
- }
- public convenience init(for unit: HKUnit) {
- self.init()
- setPreferredNumberFormatter(for: unit)
- }
- /// The unit style determines how the unit strings are abbreviated, and spacing between the value and unit
- open var unitStyle: Formatter.UnitStyle = .medium {
- didSet {
- if hasMeasurementFormatter {
- measurementFormatter.unitStyle = unitStyle
- }
- if hasMassFormatter {
- massFormatter.unitStyle = unitStyle
- }
- }
- }
- open var locale: Locale = Locale.current {
- didSet {
- if hasNumberFormatter {
- numberFormatter.locale = locale
- }
- if hasMeasurementFormatter {
- measurementFormatter.locale = locale
- }
- }
- }
- /// Updates `numberFormatter` configuration for the specified unit
- ///
- /// - Parameter unit: The unit
- open func setPreferredNumberFormatter(for unit: HKUnit) {
- numberFormatter.numberStyle = .decimal
- numberFormatter.minimumFractionDigits = unit.preferredFractionDigits
- numberFormatter.maximumFractionDigits = unit.maxFractionDigits
- }
- private var hasNumberFormatter = false
- /// The formatter used for the quantity values
- open private(set) lazy var numberFormatter: NumberFormatter = {
- hasNumberFormatter = true
- let formatter = NumberFormatter()
- formatter.numberStyle = .decimal
- formatter.locale = self.locale
- return formatter
- }()
- private var hasMeasurementFormatter = false
- /// MeasurementFormatter is used for gram measurements, mg/dL units, and mmol/L units.
- /// It does not properly handle glucose measurements, as it changes unit scales: 100 mg/dL -> 1 g/L
- private lazy var measurementFormatter: MeasurementFormatter = {
- hasMeasurementFormatter = true
- let formatter = MeasurementFormatter()
- formatter.unitOptions = [.providedUnit]
- formatter.numberFormatter = self.numberFormatter
- formatter.locale = self.locale
- formatter.unitStyle = self.unitStyle
- return formatter
- }()
- private var hasMassFormatter = false
- /// MassFormatter properly creates unit strings for grams in .short/.medium style as "g", where MeasurementFormatter uses "gram"/"grams"
- private lazy var massFormatter: MassFormatter = {
- hasMassFormatter = true
- let formatter = MassFormatter()
- formatter.numberFormatter = self.numberFormatter
- formatter.unitStyle = self.unitStyle
- return formatter
- }()
- /// Formats a quantity and unit as a localized string
- ///
- /// - Parameters:
- /// - quantity: The quantity
- /// - unit: The unit. An exception is thrown if `quantity` is not compatible with the unit.
- /// - includeUnit: Whether or not to include the unit in the returned string
- /// - Returns: A localized string, or nil if `numberFormatter` is unable to format the quantity value
- open func string(from quantity: HKQuantity, for unit: HKUnit, includeUnit: Bool = true) -> String? {
- let value = quantity.doubleValue(for: unit)
- if !includeUnit {
- return numberFormatter.string(from: value)
- }
- if let foundationUnit = unit.foundationUnit, unit.usesMeasurementFormatterForMeasurement {
- return measurementFormatter.string(from: Measurement(value: value, unit: foundationUnit))
- }
-
- return numberFormatter.string(from: value, unit: string(from: unit, forValue: value), style: unitStyle)
- }
- /// Formats a unit as a localized string
- ///
- /// - Parameters:
- /// - unit: The unit
- /// - value: An optional value for determining the plurality of the unit string
- /// - Returns: A string for the unit. If no localization entry is available, the unlocalized `unitString` is returned.
- open func string(from unit: HKUnit, forValue value: Double = 10) -> String {
- if let string = unit.localizedUnitString(in: unitStyle, singular: abs(1.0 - value) < .ulpOfOne) {
- return string
- }
- if unit.usesMassFormatterForUnitString {
- return massFormatter.unitString(fromValue: value, unit: HKUnit.massFormatterUnit(from: unit))
- }
- if let foundationUnit = unit.foundationUnit {
- return measurementFormatter.string(from: foundationUnit)
- }
- // Fallback, unlocalized
- return unit.unitString
- }
- }
- public extension HKUnit {
- var usesMassFormatterForUnitString: Bool {
- return self == .gram()
- }
- var usesMeasurementFormatterForMeasurement: Bool {
- return self == .gram()
- }
- var preferredFractionDigits: Int {
- if self == HKUnit.millimolesPerLiter || self == HKUnit.millimolesPerLiter.unitDivided(by: .internationalUnit()) {
- return 1
- } else {
- return 0
- }
- }
- var maxFractionDigits: Int {
- switch self {
- case .internationalUnit(), .internationalUnitsPerHour:
- return 3
- case HKUnit.gram().unitDivided(by: .internationalUnit()):
- return 2
- default:
- return preferredFractionDigits
- }
- }
- // Short localized unit string with unlocalized fallback
- func shortLocalizedUnitString() -> String {
- return localizedUnitString(in: .short) ?? unitString
- }
- func localizedUnitString(in style: Formatter.UnitStyle, singular: Bool = false) -> String? {
- if self == .internationalUnit() {
- switch style {
- case .short, .medium:
- return LocalizedString("U", comment: "The short unit display string for international units of insulin")
- case .long:
- fallthrough
- @unknown default:
- if singular {
- return LocalizedString("Unit", comment: "The long unit display string for a singular international unit of insulin")
- } else {
- return LocalizedString("Units", comment: "The long unit display string for international units of insulin")
- }
- }
- }
- if self == .internationalUnitsPerHour {
- switch style {
- case .short, .medium:
- return LocalizedString("U/hr", comment: "The short unit display string for international units of insulin per hour")
- case .long:
- fallthrough
- @unknown default:
- if singular {
- return LocalizedString("Unit/hour", comment: "The long unit display string for a singular international unit of insulin per hour")
- } else {
- return LocalizedString("Units/hour", comment: "The long unit display string for international units of insulin per hour")
- }
- }
- }
- if self == HKUnit.millimolesPerLiter {
- switch style {
- case .short, .medium:
- return LocalizedString("mmol/L", comment: "The short unit display string for millimoles per liter")
- case .long:
- break // Fallback to the MeasurementFormatter localization
- @unknown default:
- break
- }
- }
- if self == HKUnit.milligramsPerDeciliter.unitDivided(by: HKUnit.internationalUnit()) {
- switch style {
- case .short, .medium:
- return LocalizedString("mg/dL/U", comment: "The short unit display string for milligrams per deciliter per U")
- case .long:
- break // Fallback to the MeasurementFormatter localization
- @unknown default:
- break
- }
- }
- if self == HKUnit.millimolesPerLiter.unitDivided(by: HKUnit.internationalUnit()) {
- switch style {
- case .short, .medium:
- return LocalizedString("mmol/L/U", comment: "The short unit display string for millimoles per liter per U")
- case .long:
- break // Fallback to the MeasurementFormatter localization
- @unknown default:
- break
- }
- }
- if self == HKUnit.gram().unitDivided(by: HKUnit.internationalUnit()) {
- switch style {
- case .short, .medium:
- return LocalizedString("g/U", comment: "The short unit display string for grams per U")
- case .long:
- fallthrough
- @unknown default:
- break // Fallback to the MeasurementFormatter localization
- }
- }
- return nil
- }
- }
|