fix(goals): cap daily progress contributions to 100% to prevent weekly and monthly progress distortion
Weekly/monthly progress for a daily quest summed each day's raw value into the numerator, so one over-achieving day leaked its overage into the span total. Example: two daily quests with a 5-minute target; on one day quest A did 5 min (100%) and quest B did 10 min (200%). Both are "done" for that day, but the weekly ratio counted quest B as 600s vs quest A's 300s — B carried double the weight, and its over-achievement masked other days' shortfalls and could push per-quest weekly display past 100%. spanProgress now aggregates a daily 'atLeast' quest's week/month value as the sum of per-day values each capped at that day's target (min(dayValue, target)), via a new private spanValue(for:range:now:). This equals averaging per-day capped ratios, so a single 200% day contributes exactly one day's worth — no day can cover for another, and per-quest weekly/monthly display never exceeds 100%. Scope is deliberately narrow: - day span is untouched — a single day still shows 200% over-achievement. - 'atMost' (stay-under) quests keep their existing per-day 100%/0% logic; capping the value would make them always pass, so they use the raw value unchanged — the two rules coexist without contradiction. - weekly/monthly/custom quests accumulate freely within their period, so no per-day cap applies to them. Only the progress calculation changed; raw records are never altered, so the records/stats screens still show the true 10-minute total. Verified in the simulator with a temporary isolated in-memory self-test (removed before commit): quest A (100%) and quest B (200%) produce identical weekly ratios (0.1429 each), quest B's weekly display stays <= 100%, and its day-span display remains 200%. Build succeeds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
parent
c7024ea612
commit
4a03868163
@ -155,12 +155,43 @@ struct QuestProgress {
|
||||
case .month: range = math.monthRange(containing: now)
|
||||
}
|
||||
return QuestProgressResult(
|
||||
value: value(in: range, now: now),
|
||||
value: spanValue(for: span, range: range, now: now),
|
||||
target: scaledTarget(for: span, range: range, now: now),
|
||||
direction: quest.direction
|
||||
)
|
||||
}
|
||||
|
||||
/// span 구간의 집계 값.
|
||||
/// 주간·월간에서 '이상 달성' 하루 단위 다짐은 **하루별 기여를 그날의 목표치로 제한(캡)** 한 뒤 합산한다.
|
||||
/// 어떤 하루의 과도한 초과 달성이 다른 날의 미달을 덮어써 전체 진행률을 왜곡하는 것을 막기 위함이다.
|
||||
/// (예: 하루 목표 5분인데 어느 날 10분을 해도 그날은 5분어치만 인정 → 다른 날 미달이 가려지지 않음)
|
||||
/// 원본 기록은 그대로 두고 오직 이 계산에서만 상한을 적용하므로 기록·통계 화면의 총 누적값에는 영향이 없다.
|
||||
/// - 하루 span: 그날의 실제 누적을 그대로 써서 200% 같은 초과 달성 표기를 유지한다.
|
||||
/// - '이하 유지'(atMost): 값이 아니라 하루 초과 여부(100%/0%)로 판정되므로 캡 대상이 아니다(기존 로직 유지).
|
||||
/// - 주/월/특정기간 단위 다짐: 기간 내 자유 누적이 정상 동작이라 캡 없이 전체 누적값을 쓴다.
|
||||
private func spanValue(for span: StatSpan, range: Range<Date>, now: Date) -> Double {
|
||||
guard span != .day,
|
||||
quest.period == .daily,
|
||||
quest.direction == .atLeast,
|
||||
quest.targetValue > 0
|
||||
else {
|
||||
return value(in: range, now: now)
|
||||
}
|
||||
let dailyTarget = quest.targetValue
|
||||
let agg = Aggregator(math: math)
|
||||
var sum = 0.0
|
||||
for key in math.dayKeys(in: range) where isActiveDay(key) {
|
||||
let dayRange = math.dayRange(forKey: key)
|
||||
let dayValue: Double
|
||||
switch quest.measure {
|
||||
case .time: dayValue = agg.seconds(for: quest.targetActions, in: dayRange, now: now)
|
||||
case .count: dayValue = Double(agg.count(for: quest.targetActions, in: dayRange))
|
||||
}
|
||||
sum += min(dayValue, dailyTarget)
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
/// 주기 목표량을 span 길이에 맞춰 환산
|
||||
private func scaledTarget(for span: StatSpan, range: Range<Date>, now: Date) -> Double {
|
||||
let target = quest.targetValue
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user