缺少代码差异信息,无法生成具体的commit message。请提供code differences内容以便分析并生成符合Angular规范的提交信息。

当您提供代码差异后,我将按照以下格式生成:

```
<type>(<scope>): <subject>

<body>
```

其中type会根据更改类型选择(feat、fix、docs、style、refactor等),scope表示影响范围,subject简要描述变更内容,body详细说明修改内容。
This commit is contained in:
link2026
2026-06-07 14:17:18 +08:00
parent 074d99715d
commit 77a4ee1c37
66 changed files with 2676 additions and 548 deletions

View File

@@ -0,0 +1,181 @@
import Foundation
/// ##
///
/// `docs/superpowers/specs/2026-06-07-export-indicator-trends-design.md`:
/// 2 ,
/// ( + + + ),** LLM**, `ReportCompareService` ,
/// (§10#5 退 / §12#6 )
struct ExportTrend: Sendable {
enum Direction: Sendable {
case up, down, flat
var arrow: String {
switch self {
case .up: return ""
case .down: return ""
case .flat: return ""
}
}
}
let title: String
let unit: String
/// "152138" "152/96138/88"
let valueText: String
let direction: Direction
/// , "90-140";( / ) nil
let rangeText: String?
///
let spanDays: Int
///
let count: Int
/// ,
let flagged: Bool
/// :` 152138 mmHg 90-140 21 4 `
func line() -> String {
var s = flagged ? "⚠️ " : ""
s += title
s += " \(valueText)"
if !unit.isEmpty { s += " \(unit)" }
s += " \(direction.arrow)"
if let r = rangeText, !r.isEmpty { s += "(参考 \(r)" }
s += ",近 \(spanDays)\(count)"
return s
}
}
enum ExportTrendBuilder {
/// : < 5% ()
static let flatThreshold = 0.05
///
/// - Parameters:
/// - allInWindow: ****()
/// - relevant: ****() series
/// - profile: ( SeriesBucket)
/// - customMetrics: , series /
/// - Returns: ,
static func build(allInWindow: [Indicator],
relevant: [Indicator],
profile: UserProfile? = nil,
customMetrics: [CustomMonitorMetric] = []) -> [ExportTrend] {
let relevantIDs = Set(relevant.compactMap { bucketID(for: $0) })
guard !relevantIDs.isEmpty else { return [] }
// Trends : seriesKey name+unit 退minPoints2
let buckets = SeriesBucket.build(from: allInWindow,
profile: profile,
customMetrics: customMetrics,
minPoints: 2)
let trends = buckets
.filter { relevantIDs.contains($0.id) }
.compactMap { trend(from: $0) }
// ,
return trends.sorted { lhs, rhs in
if lhs.flagged != rhs.flagged { return lhs.flagged }
return lhs.spanDays >= rhs.spanDays // , buckets
}
}
/// SeriesBucket id( `SeriesBucket.build` id )
/// nil series()
static func bucketID(for i: Indicator) -> String? {
if let k = i.seriesKey, !k.isEmpty {
if k == "bp.systolic" || k == "bp.diastolic" { return "bp" }
return k
}
let nk = SeriesBucket.normalizedKey(name: i.name, unit: i.unit)
return nk.isEmpty ? nil : "lab:\(nk)"
}
// MARK: - Private
private static func trend(from bucket: SeriesBucket) -> ExportTrend? {
if bucket.id == "bp" { return bpTrend(from: bucket) }
guard let line = bucket.lines.first,
line.points.count >= 2,
let first = line.points.first,
let last = line.points.last else { return nil }
return ExportTrend(
title: bucket.title,
unit: bucket.unit,
valueText: "\(num(first.value))\(num(last.value))",
direction: direction(first: first.value, last: last.value),
rangeText: rangeText(line.referenceRange),
spanDays: spanDays(first.date, last.date),
count: line.points.count,
flagged: last.status != .normal
|| crossedBoundary(first: first.status, last: last.status)
)
}
/// : + ,;(/,)
private static func bpTrend(from bucket: SeriesBucket) -> ExportTrend? {
guard let sys = bucket.lines.first(where: { $0.seriesKey == "bp.systolic" }),
sys.points.count >= 2,
let sFirst = sys.points.first,
let sLast = sys.points.last else { return nil }
let dia = bucket.lines.first { $0.seriesKey == "bp.diastolic" }
let dFirst = dia?.points.first
let dLast = dia?.points.last
let valueText: String
if let dFirst, let dLast {
valueText = "\(num(sFirst.value))/\(num(dFirst.value))\(num(sLast.value))/\(num(dLast.value))"
} else {
valueText = "\(num(sFirst.value))\(num(sLast.value))"
}
let sysFlag = sLast.status != .normal
|| crossedBoundary(first: sFirst.status, last: sLast.status)
let diaFlag = dLast.map { $0.status != .normal } ?? false
return ExportTrend(
title: bucket.title,
unit: bucket.unit,
valueText: valueText,
direction: direction(first: sFirst.value, last: sLast.value),
rangeText: nil,
spanDays: spanDays(sFirst.date, sLast.date),
count: sys.points.count,
flagged: sysFlag || diaFlag
)
}
static func direction(first: Double, last: Double) -> ExportTrend.Direction {
let delta = last - first
let base = abs(first)
let rel = base > 0 ? abs(delta) / base : abs(delta)
if rel < flatThreshold { return .flat }
return delta > 0 ? .up : .down
}
/// ()
static func crossedBoundary(first: IndicatorStatus, last: IndicatorStatus) -> Bool {
(first == .normal) != (last == .normal)
}
static func spanDays(_ from: Date, _ to: Date) -> Int {
let days = to.timeIntervalSince(from) / 86400
return max(1, Int(days.rounded()))
}
static func rangeText(_ r: ClosedRange<Double>?) -> String? {
guard let r else { return nil }
return "\(num(r.lowerBound))-\(num(r.upperBound))"
}
/// :, 0(138.0"138",6.10"6.1")
static func num(_ v: Double) -> String {
if v.truncatingRemainder(dividingBy: 1) == 0 { return String(Int(v)) }
return String(format: "%g", v)
}
}