1+ /*
2+ * openScale
3+ * Copyright (C) 2025 olie.xdev <olie.xdeveloper@googlemail.com>
4+ *
5+ * This program is free software: you can redistribute it and/or modify
6+ * it under the terms of the GNU General Public License as published by
7+ * the Free Software Foundation, either version 3 of the License, or
8+ * (at your option) any later version.
9+ *
10+ * This program is distributed in the hope that it will be useful,
11+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+ * GNU General Public License for more details.
14+ *
15+ * You should have received a copy of the GNU General Public License
16+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
17+ */
18+ package com.health.openscale.core.model
19+
20+ import androidx.compose.runtime.Immutable
21+ import com.health.openscale.core.data.MeasurementType
22+ import java.time.DayOfWeek
23+ import java.time.LocalDate
24+ import java.time.Month
25+
26+ /* *
27+ * Represents the confidence level of a computed [MeasurementInsight].
28+ *
29+ * - [HIGH]: Sufficient, regular data — conclusions are reliable.
30+ * - [LOW]: Limited or irregular data — conclusions should be treated with caution.
31+ * - [INSUFFICIENT]: Not enough data to draw any meaningful conclusion — insight is hidden.
32+ */
33+ enum class InsightConfidence { HIGH , LOW , INSUFFICIENT }
34+
35+ /* * Direction of a long- or short-term value trend. */
36+ enum class ShiftTrend { UP , DOWN , STABLE }
37+
38+ /* *
39+ * How much a value fluctuates around its mean, derived from its standard deviation
40+ * relative to the overall value range.
41+ *
42+ * - [STABLE]: Low fluctuation — the value changes gradually and predictably.
43+ * - [MODERATE]: Medium fluctuation — some variation but no extreme swings.
44+ * - [HIGH]: High fluctuation — the value swings significantly day to day.
45+ */
46+ enum class Volatility { STABLE , MODERATE , HIGH }
47+
48+ /* *
49+ * A rich analysis of how a single measurement type has evolved over the user's
50+ * full measurement history.
51+ *
52+ * Beyond a simple first-to-last delta, this captures the rate of change,
53+ * plateau detection, best period, and short- vs. long-term trend direction —
54+ * giving the user actionable context rather than raw numbers.
55+ *
56+ * @property type The measurement type being analysed.
57+ * @property firstValue Value of the first recorded measurement.
58+ * @property lastValue Value of the most recent measurement.
59+ * @property deltaAbsolute Signed absolute change: [lastValue] − [firstValue].
60+ * @property deltaPercent Relative change in percent: ([deltaAbsolute] / [firstValue]) × 100.
61+ * @property minValue Lowest recorded value across the full history.
62+ * @property minValueDate Date on which [minValue] was recorded.
63+ * @property maxValue Highest recorded value across the full history.
64+ * @property maxValueDate Date on which [maxValue] was recorded.
65+ * @property volatility How much the value fluctuates around its mean.
66+ * @property shortTermTrend Trend direction computed over the last 30 days of data.
67+ * @property longTermTrend Trend direction computed over the full history.
68+ * @property ratePerMonth Average absolute change per calendar month
69+ * (positive = increasing, negative = decreasing).
70+ * @property plateauDays Number of consecutive days without a significant change
71+ * (> 0.5 % of mean) at the end of the history, or null if
72+ * no plateau is currently active.
73+ * @property bestPeriodStart Start of the calendar month with the largest improvement,
74+ * null if fewer than two months of data are available.
75+ * @property bestPeriodDelta The signed delta achieved in [bestPeriodStart]'s month,
76+ * null when [bestPeriodStart] is null.
77+ * @property firstMeasuredOn Date of the first measurement included in this analysis.
78+ * @property lastMeasuredOn Date of the most recent measurement included.
79+ * @property confidence Overall reliability of this insight.
80+ */
81+ @Immutable
82+ data class BodyCompositionShift (
83+ val type : MeasurementType ,
84+ val firstValue : Float ,
85+ val lastValue : Float ,
86+ val deltaAbsolute : Float ,
87+ val deltaPercent : Float ,
88+ val minValue : Float ,
89+ val minValueDate : LocalDate ,
90+ val maxValue : Float ,
91+ val maxValueDate : LocalDate ,
92+ val volatility : Volatility ,
93+ val shortTermTrend : ShiftTrend ,
94+ val longTermTrend : ShiftTrend ,
95+ val ratePerMonth : Float ,
96+ val plateauDays : Int? ,
97+ val bestPeriodStart : LocalDate ? ,
98+ val bestPeriodDelta : Float? ,
99+ val firstMeasuredOn : LocalDate ,
100+ val lastMeasuredOn : LocalDate ,
101+ val confidence : InsightConfidence ,
102+ )
103+
104+ /* *
105+ * Represents the average value deviation per day of the week for a specific measurement type.
106+ *
107+ * The primary type is chosen by the caller rather than being hardcoded,
108+ * so custom types are fully supported.
109+ *
110+ * Requires at least [MIN_MEASUREMENTS_PER_DAY] measurements per weekday to produce
111+ * a [InsightConfidence.HIGH] confidence result.
112+ *
113+ * @property type The measurement type this pattern is based on.
114+ * @property overallMean Average value across all days of the week.
115+ * @property deviationByDay Map of [DayOfWeek] to average deviation from the overall mean
116+ * (positive = above average, negative = below average).
117+ * @property measurementCountByDay Number of measurements available per weekday.
118+ * @property heaviestDay Weekday with the highest average value, null if insufficient data.
119+ * @property lightestDay Weekday with the lowest average value, null if insufficient data.
120+ * @property confidence Reliability of this insight based on data availability.
121+ */
122+ @Immutable
123+ data class WeekdayPattern (
124+ val type : MeasurementType ,
125+ val overallMean : Float ,
126+ val deviationByDay : Map <DayOfWeek , Float >,
127+ val measurementCountByDay : Map <DayOfWeek , Int >,
128+ val heaviestDay : DayOfWeek ? ,
129+ val lightestDay : DayOfWeek ? ,
130+ val confidence : InsightConfidence ,
131+ ) {
132+ companion object {
133+ /* * Minimum measurements per weekday for [InsightConfidence.HIGH]. */
134+ const val MIN_MEASUREMENTS_PER_DAY = 5
135+ }
136+ }
137+
138+ /* *
139+ * Represents the average value per calendar month grouped by year for a specific measurement type.
140+ * Used to detect recurring seasonal patterns across multiple years.
141+ *
142+ * Requires data spanning at least [MIN_YEARS_FOR_PATTERN] years to produce
143+ * a [InsightConfidence.HIGH] confidence result.
144+ *
145+ * @property type The measurement type this pattern is based on.
146+ * @property averageValueByMonthAndYear Nested map of year → month → average value in the type's unit.
147+ * @property highestMonth Month with the highest cross-year average, null if insufficient.
148+ * @property lowestMonth Month with the lowest cross-year average, null if insufficient.
149+ * @property yearsWithData Number of distinct years covered by the data.
150+ * @property confidence Reliability of this insight based on data availability.
151+ */
152+ @Immutable
153+ data class SeasonalPattern (
154+ val type : MeasurementType ,
155+ val averageValueByMonthAndYear : Map <Int , Map <Month , Float >>,
156+ val highestMonth : Month ? ,
157+ val lowestMonth : Month ? ,
158+ val yearsWithData : Int ,
159+ val confidence : InsightConfidence ,
160+ ) {
161+ companion object {
162+ /* * Minimum distinct years required for [InsightConfidence.HIGH]. */
163+ const val MIN_YEARS_FOR_PATTERN = 2
164+ }
165+ }
166+
167+ /* *
168+ * A single detected anomaly in a measurement series.
169+ *
170+ * Detected via a rolling z-score over a sliding window of recent measurements.
171+ * A gap longer than [com.health.openscale.core.usecase.MeasurementInsightsUseCase.ANOMALY_GAP_RESET_DAYS]
172+ * resets the baseline to avoid false positives after measurement breaks.
173+ *
174+ * @property measurementId ID of the measurement that triggered the anomaly.
175+ * @property date Date of the anomalous measurement.
176+ * @property type The [MeasurementType] in which the anomaly was detected.
177+ * @property value The actual measured value.
178+ * @property expectedValue Expected value based on the local rolling average.
179+ * @property deviation Signed difference between [value] and [expectedValue].
180+ * @property zScore Standardised deviation — values with |zScore| ≥ threshold are flagged.
181+ * @property comment Optional user comment on that measurement date, if any.
182+ */
183+ @Immutable
184+ data class MeasurementAnomaly (
185+ val measurementId : Int ,
186+ val date : LocalDate ,
187+ val type : MeasurementType ,
188+ val value : Float ,
189+ val expectedValue : Float ,
190+ val deviation : Float ,
191+ val zScore : Float ,
192+ val comment : String? ,
193+ )
194+
195+ /* *
196+ * Top-level container for all computed insights derived from a user's measurement history.
197+ *
198+ * Each insight field is nullable — null means [InsightConfidence.INSUFFICIENT] data
199+ * for that specific analysis.
200+ *
201+ * Note: [MeasurementInsight] sits alongside the aggregation hierarchy rather than
202+ * extending it — it is derived from the full measurement history, not from a single entry.
203+ *
204+ * @property bodyCompositionShift Rich analysis of how the primary type evolved over time, or null.
205+ * @property weekdayPattern Average deviation per weekday for the primary type, or null.
206+ * @property seasonalPattern Average value per month grouped by year, or null.
207+ * @property anomalies Detected anomalies sorted by date descending, empty if none found.
208+ * @property basedOnCount Total raw measurements used to compute these insights.
209+ * @property computedAt Date on which these insights were last computed.
210+ */
211+ @Immutable
212+ data class MeasurementInsight (
213+ val bodyCompositionShift : BodyCompositionShift ? ,
214+ val weekdayPattern : WeekdayPattern ? ,
215+ val seasonalPattern : SeasonalPattern ? ,
216+ val anomalies : List <MeasurementAnomaly >,
217+ val basedOnCount : Int ,
218+ val computedAt : LocalDate ,
219+ )
0 commit comments