BareStatisticsView.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. import CoreData
  2. import SwiftDate
  3. import SwiftUI
  4. struct BareStatisticsView {
  5. // MARK: - Helper Functions
  6. private static func medianCalculation(array: [Int]) -> Double {
  7. guard !array.isEmpty else { return 0 }
  8. let sorted = array.sorted()
  9. let length = array.count
  10. if length % 2 == 0 {
  11. return Double((sorted[length / 2 - 1] + sorted[length / 2]) / 2)
  12. }
  13. return Double(sorted[length / 2])
  14. }
  15. static func medianCalculationDouble(array: [Double]) -> Double {
  16. guard !array.isEmpty else { return 0 }
  17. let sorted = array.sorted()
  18. let length = array.count
  19. if length % 2 == 0 {
  20. return (sorted[length / 2 - 1] + sorted[length / 2]) / 2
  21. }
  22. return sorted[length / 2]
  23. }
  24. struct HbA1cView: View {
  25. let highLimit: Decimal
  26. let lowLimit: Decimal
  27. let units: GlucoseUnits
  28. let hbA1cDisplayUnit: HbA1cDisplayUnit
  29. let glucose: [GlucoseStored]
  30. var body: some View {
  31. hba1c
  32. }
  33. private var hba1c: some View {
  34. HStack(spacing: 50) {
  35. let useUnit: GlucoseUnits = {
  36. if hbA1cDisplayUnit == .mmolMol { return .mmolL }
  37. else { return .mgdL }
  38. }()
  39. let hba1cs = glucoseStats()
  40. // First date
  41. let previous = glucose.last?.date ?? Date()
  42. // Last date (recent)
  43. let current = glucose.first?.date ?? Date()
  44. // Total time in days
  45. let numberOfDays = (current - previous).timeInterval / 8.64E4
  46. let hba1cString = (
  47. useUnit == .mmolL ? hba1cs.ifcc
  48. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) : hba1cs.ngsp
  49. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
  50. + " %"
  51. )
  52. VStack(spacing: 5) {
  53. Text("HbA1c").font(.subheadline).foregroundColor(.secondary)
  54. Text(hba1cString)
  55. }
  56. VStack(spacing: 5) {
  57. Text("SD").font(.subheadline).foregroundColor(.secondary)
  58. Text(
  59. hba1cs.sd
  60. .formatted(
  61. .number.grouping(.never).rounded()
  62. .precision(.fractionLength(units == .mmolL ? 1 : 0))
  63. )
  64. )
  65. }
  66. VStack(spacing: 5) {
  67. Text("CV").font(.subheadline).foregroundColor(.secondary)
  68. Text(hba1cs.cv.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))))
  69. }
  70. VStack(spacing: 5) {
  71. Text("Days").font(.subheadline).foregroundColor(.secondary)
  72. Text(numberOfDays.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))))
  73. }
  74. }
  75. }
  76. func glucoseStats()
  77. -> (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
  78. {
  79. // First date
  80. let previous = glucose.last?.date ?? Date()
  81. // Last date (recent)
  82. let current = glucose.first?.date ?? Date()
  83. // Total time in days
  84. let numberOfDays = (current - previous).timeInterval / 8.64E4
  85. let denominator = numberOfDays < 1 ? 1 : numberOfDays
  86. let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
  87. let sumReadings = justGlucoseArray.reduce(0, +)
  88. let countReadings = justGlucoseArray.count
  89. let glucoseAverage = Double(sumReadings) / Double(countReadings)
  90. let medianGlucose = BareStatisticsView.medianCalculation(array: justGlucoseArray)
  91. var NGSPa1CStatisticValue = 0.0
  92. var IFCCa1CStatisticValue = 0.0
  93. if numberOfDays > 0 {
  94. NGSPa1CStatisticValue = (glucoseAverage + 46.7) / 28.7
  95. IFCCa1CStatisticValue = 10.929 * (NGSPa1CStatisticValue - 2.152)
  96. }
  97. var sumOfSquares = 0.0
  98. for array in justGlucoseArray {
  99. sumOfSquares += pow(Double(array) - Double(glucoseAverage), 2)
  100. }
  101. var sd = 0.0
  102. var cv = 0.0
  103. if glucoseAverage > 0 {
  104. sd = sqrt(sumOfSquares / Double(countReadings))
  105. cv = sd / Double(glucoseAverage) * 100
  106. }
  107. return (
  108. ifcc: IFCCa1CStatisticValue,
  109. ngsp: NGSPa1CStatisticValue,
  110. average: glucoseAverage * (units == .mmolL ? 0.0555 : 1),
  111. median: medianGlucose * (units == .mmolL ? 0.0555 : 1),
  112. sd: sd * (units == .mmolL ? 0.0555 : 1),
  113. cv: cv,
  114. readings: Double(countReadings) / denominator
  115. )
  116. }
  117. }
  118. struct BloodGlucoseView: View {
  119. let highLimit: Decimal
  120. let lowLimit: Decimal
  121. let units: GlucoseUnits
  122. let hbA1cDisplayUnit: HbA1cDisplayUnit
  123. let glucose: [GlucoseStored]
  124. var body: some View {
  125. bloodGlucose
  126. }
  127. private var bloodGlucose: some View {
  128. HStack(spacing: 30) {
  129. let bgs = glucoseStats()
  130. // First date
  131. let previous = glucose.last?.date ?? Date()
  132. // Last date (recent)
  133. let current = glucose.first?.date ?? Date()
  134. // Total time in days
  135. let numberOfDays = (current - previous).timeInterval / 8.64E4
  136. VStack(spacing: 5) {
  137. Text(numberOfDays < 1 ? "Readings" : "Readings / 24 h").font(.subheadline)
  138. .foregroundColor(.secondary)
  139. Text(bgs.readings.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))))
  140. }
  141. VStack(spacing: 5) {
  142. Text("Average").font(.subheadline).foregroundColor(.secondary)
  143. Text(
  144. bgs.average
  145. .formatted(
  146. .number.grouping(.never).rounded()
  147. .precision(.fractionLength(units == .mmolL ? 1 : 0))
  148. )
  149. )
  150. }
  151. VStack(spacing: 5) {
  152. Text("Median").font(.subheadline).foregroundColor(.secondary)
  153. Text(
  154. bgs.median
  155. .formatted(
  156. .number.grouping(.never).rounded()
  157. .precision(.fractionLength(units == .mmolL ? 1 : 0))
  158. )
  159. )
  160. }
  161. }
  162. }
  163. func glucoseStats()
  164. -> (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
  165. {
  166. // First date
  167. let previous = glucose.last?.date ?? Date()
  168. // Last date (recent)
  169. let current = glucose.first?.date ?? Date()
  170. // Total time in days
  171. let numberOfDays = (current - previous).timeInterval / 8.64E4
  172. let denominator = numberOfDays < 1 ? 1 : numberOfDays
  173. let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
  174. let sumReadings = justGlucoseArray.reduce(0, +)
  175. let countReadings = justGlucoseArray.count
  176. let glucoseAverage = Double(sumReadings) / Double(countReadings)
  177. let medianGlucose = BareStatisticsView.medianCalculation(array: justGlucoseArray)
  178. var NGSPa1CStatisticValue = 0.0
  179. var IFCCa1CStatisticValue = 0.0
  180. if numberOfDays > 0 {
  181. NGSPa1CStatisticValue = (glucoseAverage + 46.7) / 28.7
  182. IFCCa1CStatisticValue = 10.929 * (NGSPa1CStatisticValue - 2.152)
  183. }
  184. var sumOfSquares = 0.0
  185. for array in justGlucoseArray {
  186. sumOfSquares += pow(Double(array) - Double(glucoseAverage), 2)
  187. }
  188. var sd = 0.0
  189. var cv = 0.0
  190. if glucoseAverage > 0 {
  191. sd = sqrt(sumOfSquares / Double(countReadings))
  192. cv = sd / Double(glucoseAverage) * 100
  193. }
  194. return (
  195. ifcc: IFCCa1CStatisticValue,
  196. ngsp: NGSPa1CStatisticValue,
  197. average: glucoseAverage * (units == .mmolL ? 0.0555 : 1),
  198. median: medianGlucose * (units == .mmolL ? 0.0555 : 1),
  199. sd: sd * (units == .mmolL ? 0.0555 : 1),
  200. cv: cv,
  201. readings: Double(countReadings) / denominator
  202. )
  203. }
  204. }
  205. struct LoopsView: View {
  206. let highLimit: Decimal
  207. let lowLimit: Decimal
  208. let units: GlucoseUnits
  209. let hbA1cDisplayUnit: HbA1cDisplayUnit
  210. let loopStatRecords: [LoopStatRecord]
  211. var body: some View {
  212. loops
  213. }
  214. private var loops: some View {
  215. let loops = loopStatRecords
  216. // First date
  217. let previous = loops.last?.end ?? Date()
  218. // Last date (recent)
  219. let current = loops.first?.start ?? Date()
  220. // Total time in days
  221. let totalTime = (current - previous).timeInterval / 8.64E4
  222. let durationArray = loops.compactMap({ each in each.duration })
  223. let durationArrayCount = durationArray.count
  224. // var durationAverage = durationArray.reduce(0, +) / Double(durationArrayCount)
  225. let medianDuration = medianCalculationDouble(array: durationArray)
  226. let successsNR = loops.compactMap({ each in each.loopStatus }).filter({ each in each!.contains("Success") }).count
  227. let errorNR = durationArrayCount - successsNR
  228. let total = Double(successsNR + errorNR) == 0 ? 1 : Double(successsNR + errorNR)
  229. let successRate: Double? = (Double(successsNR) / total) * 100
  230. let loopNr = totalTime <= 1 ? total : round(total / (totalTime != 0 ? totalTime : 1))
  231. let intervalArray = loops.compactMap({ each in each.interval as Double })
  232. let count = intervalArray.count != 0 ? intervalArray.count : 1
  233. let intervalAverage = intervalArray.reduce(0, +) / Double(count)
  234. // let maximumInterval = intervalArray.max()
  235. // let minimumInterval = intervalArray.min()
  236. return VStack(spacing: 10) {
  237. HStack(spacing: 35) {
  238. VStack(spacing: 5) {
  239. Text("Loops").font(.subheadline).foregroundColor(.primary)
  240. Text(loopNr.formatted())
  241. }
  242. VStack(spacing: 5) {
  243. Text("Interval").font(.subheadline).foregroundColor(.primary)
  244. Text(intervalAverage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + " min")
  245. }
  246. VStack(spacing: 5) {
  247. Text("Duration").font(.subheadline).foregroundColor(.primary)
  248. Text(
  249. (medianDuration / 1000)
  250. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + " s"
  251. )
  252. }
  253. VStack(spacing: 5) {
  254. Text("Success").font(.subheadline).foregroundColor(.primary)
  255. Text(
  256. ((successRate ?? 100) / 100)
  257. .formatted(.percent.grouping(.never).rounded().precision(.fractionLength(1)))
  258. )
  259. }
  260. }
  261. }
  262. }
  263. private func medianCalculationDouble(array: [Double]) -> Double {
  264. BareStatisticsView.medianCalculationDouble(array: array)
  265. }
  266. }
  267. }