Localizer.swift 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. // LoopFollow
  2. // Localizer.swift
  3. import Foundation
  4. import HealthKit
  5. enum GlucoseDisplayUnit: String, Codable, CaseIterable {
  6. case mgdL = "mg/dL"
  7. case mmolL = "mmol/L"
  8. var fractionDigits: Int {
  9. switch self {
  10. case .mgdL:
  11. return 0
  12. case .mmolL:
  13. return 1
  14. }
  15. }
  16. }
  17. enum TimeInRangeDisplayMode: String, Codable, CaseIterable {
  18. case tir = "TIR"
  19. case titr = "TITR"
  20. case custom = "Custom"
  21. }
  22. enum GlycemicMetricMode: String, Codable, CaseIterable {
  23. case ehba1c = "eHbA1c"
  24. case gmi = "GMI"
  25. }
  26. enum GlycemicOutputUnit: String, Codable, CaseIterable {
  27. case percent = "%"
  28. case mmolMol = "mmol/mol"
  29. }
  30. enum VariabilityMetricMode: String, Codable, CaseIterable {
  31. case stdDeviation = "Std Deviation"
  32. case cv = "CV"
  33. }
  34. final class UnitSettingsStore {
  35. static let shared = UnitSettingsStore()
  36. private init() {}
  37. var glucoseUnit: GlucoseDisplayUnit {
  38. get {
  39. GlucoseDisplayUnit(rawValue: Storage.shared.units.value) ?? .mgdL
  40. }
  41. set {
  42. Storage.shared.units.value = newValue.rawValue
  43. }
  44. }
  45. var timeInRangeMode: TimeInRangeDisplayMode {
  46. get {
  47. TimeInRangeDisplayMode(rawValue: Storage.shared.timeInRangeModeRaw.value) ?? .tir
  48. }
  49. set {
  50. Storage.shared.timeInRangeModeRaw.value = newValue.rawValue
  51. }
  52. }
  53. /// Returns the effective low/high thresholds (in mg/dL) for the current range mode.
  54. func effectiveThresholds() -> (low: Double, high: Double) {
  55. switch timeInRangeMode {
  56. case .tir:
  57. return (70.0, 180.0)
  58. case .titr:
  59. return (70.0, 140.0)
  60. case .custom:
  61. return (Storage.shared.lowLine.value, Storage.shared.highLine.value)
  62. }
  63. }
  64. var glycemicMetricMode: GlycemicMetricMode {
  65. get {
  66. Storage.shared.showGMI.value ? .gmi : .ehba1c
  67. }
  68. set {
  69. Storage.shared.showGMI.value = (newValue == .gmi)
  70. }
  71. }
  72. var glycemicOutputUnit: GlycemicOutputUnit {
  73. get {
  74. Storage.shared.useIFCC.value ? .mmolMol : .percent
  75. }
  76. set {
  77. Storage.shared.useIFCC.value = (newValue == .mmolMol)
  78. }
  79. }
  80. var variabilityMetricMode: VariabilityMetricMode {
  81. get {
  82. Storage.shared.showStdDev.value ? .stdDeviation : .cv
  83. }
  84. set {
  85. Storage.shared.showStdDev.value = (newValue == .stdDeviation)
  86. }
  87. }
  88. func convertMgdlToDisplay(_ mgdl: Double) -> Double {
  89. switch glucoseUnit {
  90. case .mgdL:
  91. return mgdl
  92. case .mmolL:
  93. return mgdl * GlucoseConversion.mgDlToMmolL
  94. }
  95. }
  96. func convertDisplayToMgdl(_ value: Double) -> Double {
  97. switch glucoseUnit {
  98. case .mgdL:
  99. return value
  100. case .mmolL:
  101. return value * GlucoseConversion.mmolToMgDl
  102. }
  103. }
  104. }
  105. enum GlycemicMetricCalculator {
  106. static func calculateEhba1c(avgGlucoseInDisplayUnits: Double?) -> Double? {
  107. guard let avgGlucoseInDisplayUnits else { return nil }
  108. switch UnitSettingsStore.shared.glycemicOutputUnit {
  109. case .mmolMol:
  110. let avgMmolL = UnitSettingsStore.shared.glucoseUnit == .mmolL
  111. ? avgGlucoseInDisplayUnits
  112. : avgGlucoseInDisplayUnits * GlucoseConversion.mgDlToMmolL
  113. return avgMmolL * 6.936514699616532 - 6.628248828291433
  114. case .percent:
  115. let avgMgdL = UnitSettingsStore.shared.convertDisplayToMgdl(avgGlucoseInDisplayUnits)
  116. return (avgMgdL + 46.7) / 28.7
  117. }
  118. }
  119. static func calculateGMI(avgGlucoseInDisplayUnits: Double?) -> Double? {
  120. guard let avgGlucoseInDisplayUnits else { return nil }
  121. switch UnitSettingsStore.shared.glycemicOutputUnit {
  122. case .percent:
  123. let avgMgdL = UnitSettingsStore.shared.convertDisplayToMgdl(avgGlucoseInDisplayUnits)
  124. return 3.31 + (0.02392 * avgMgdL)
  125. case .mmolMol:
  126. let avgMmolL = UnitSettingsStore.shared.glucoseUnit == .mmolL
  127. ? avgGlucoseInDisplayUnits
  128. : avgGlucoseInDisplayUnits * GlucoseConversion.mgDlToMmolL
  129. return 12.71 + (4.70587 * avgMmolL)
  130. }
  131. }
  132. }
  133. class Localizer {
  134. static func getPreferredUnit() -> HKUnit {
  135. switch UnitSettingsStore.shared.glucoseUnit {
  136. case .mmolL:
  137. return .millimolesPerLiter
  138. case .mgdL:
  139. return .milligramsPerDeciliter
  140. }
  141. }
  142. static func formatToLocalizedString(_ value: Double, maxFractionDigits: Int = 1, minFractionDigits: Int = 0) -> String {
  143. let numberFormatter = NumberFormatter()
  144. numberFormatter.numberStyle = .decimal
  145. numberFormatter.maximumFractionDigits = maxFractionDigits
  146. numberFormatter.minimumFractionDigits = minFractionDigits
  147. numberFormatter.locale = Locale.current
  148. let numberValue = NSNumber(value: value)
  149. return numberFormatter.string(from: numberValue) ?? String(value)
  150. }
  151. static func formatQuantity(_ quantity: HKQuantity) -> String {
  152. let unit: HKUnit = getPreferredUnit()
  153. let value = quantity.doubleValue(for: unit)
  154. return formatToLocalizedString(value, maxFractionDigits: unit.preferredFractionDigits, minFractionDigits: unit.preferredFractionDigits)
  155. }
  156. static func formatQuantity(_ value: Double) -> String {
  157. formatQuantity(HKQuantity(unit: .milligramsPerDeciliter, doubleValue: value))
  158. }
  159. static func formatTimestampToLocalString(_ timestamp: TimeInterval) -> String {
  160. let date = Date(timeIntervalSince1970: timestamp)
  161. let dateFormatter = DateFormatter()
  162. dateFormatter.setLocalizedDateFormatFromTemplate("jms")
  163. dateFormatter.locale = Locale.current
  164. dateFormatter.timeZone = TimeZone.current
  165. return dateFormatter.string(from: date)
  166. }
  167. static func formatLocalDouble(_ value: Double, unit: String? = nil) -> String {
  168. let numberFormatter = NumberFormatter()
  169. numberFormatter.numberStyle = .decimal
  170. let units = GlucoseDisplayUnit(rawValue: unit ?? UnitSettingsStore.shared.glucoseUnit.rawValue) ?? .mgdL
  171. numberFormatter.maximumFractionDigits = units.fractionDigits
  172. numberFormatter.minimumFractionDigits = units.fractionDigits
  173. numberFormatter.locale = Locale.current
  174. let numberValue = NSNumber(value: value)
  175. return numberFormatter.string(from: numberValue) ?? String(value)
  176. }
  177. static func toDisplayUnits(_ value: String) -> String {
  178. let numberFormatter = NumberFormatter()
  179. numberFormatter.numberStyle = .decimal
  180. let units = UnitSettingsStore.shared.glucoseUnit
  181. numberFormatter.maximumFractionDigits = units.fractionDigits
  182. numberFormatter.minimumFractionDigits = units.fractionDigits
  183. numberFormatter.locale = Locale.current
  184. if let number = Float(value) {
  185. if units == .mgdL {
  186. let numberValue = NSNumber(value: number)
  187. return numberFormatter.string(from: numberValue) ?? value
  188. } else {
  189. let mmolValue = Double(number) * GlucoseConversion.mgDlToMmolL // Convert number to Double
  190. let numberValue = NSNumber(value: mmolValue)
  191. return numberFormatter.string(from: numberValue) ?? value
  192. }
  193. }
  194. return value
  195. }
  196. static func removePeriodAndCommaForBadge(_ value: String) -> String {
  197. var modifiedValue = value
  198. modifiedValue = modifiedValue.replacingOccurrences(of: ".", with: "")
  199. modifiedValue = modifiedValue.replacingOccurrences(of: ",", with: "")
  200. return modifiedValue
  201. }
  202. }
  203. extension Float {
  204. // remove the decimal part of the float if it is ".0" and trim whitespaces
  205. var cleanValue: String {
  206. return truncatingRemainder(dividingBy: 1) == 0
  207. ? String(format: "%5.0f", self).trimmingCharacters(in: CharacterSet.whitespaces)
  208. : String(format: "%5.1f", self).trimmingCharacters(in: CharacterSet.whitespaces)
  209. }
  210. var roundTo3f: Float {
  211. return round(to: 3)
  212. }
  213. func round(to places: Int) -> Float {
  214. let divisor = pow(10.0, Float(places))
  215. return (divisor * self).rounded() / divisor
  216. }
  217. }