///|
/// Parser error representation.
pub suberror HrvParseError {
InvalidNumber(input~ : String)
InvalidRow(msg~ : String)
EmptyInput
} derive(Debug, ToJson, FromJson)
///|
/// Trim whitespace from String to StringView
fn trim(s : String) -> StringView {
let len = s.length()
let mut start = 0
while start < len &&
(s[start] == 32 || s[start] == 9 || s[start] == 10 || s[start] == 13) {
start += 1
}
let mut end = len
while end > start &&
(
s[end - 1] == 32 ||
s[end - 1] == 9 ||
s[end - 1] == 10 ||
s[end - 1] == 13
) {
end -= 1
}
s[start:end]
}
///|
/// CSV Parser using a state-machine. Handles optional double quotes and escapes.
pub fn parse_csv(content : String) -> Array[Array[String]] {
let result = []
let mut row = []
let mut cell = StringBuilder::new()
let mut in_quotes = false
let len = content.length()
let mut i = 0
while i < len {
let c = content[i]
if in_quotes {
if c == 34 {
if i + 1 < len && content[i + 1] == 34 {
cell.write_char('"')
i += 1
} else {
in_quotes = false
}
} else {
cell.write_char(c.to_int().unsafe_to_char())
}
} else if c == 34 {
in_quotes = true
} else if c == 44 {
row.push(cell.to_string())
cell = StringBuilder::new()
} else if c == 10 {
row.push(cell.to_string())
cell = StringBuilder::new()
result.push(row)
row = []
} else if c == 13 {
if i + 1 < len && content[i + 1] == 10 {
row.push(cell.to_string())
cell = StringBuilder::new()
result.push(row)
row = []
i += 1
} else {
cell.write_char(c.to_int().unsafe_to_char())
}
} else {
cell.write_char(c.to_int().unsafe_to_char())
}
i += 1
}
if cell.to_string().length() > 0 || row.length() > 0 {
row.push(cell.to_string())
result.push(row)
}
result
}
///|
/// CSV Serializer. Escapes double quotes, commas, and newlines.
pub fn to_csv(grid : Array[Array[String]]) -> String {
let sb = StringBuilder::new()
for r in 0.. 0 {
sb.write_string(",")
}
let cell = row[c]
let needs_quotes = cell.contains(",") ||
cell.contains("\"") ||
cell.contains("\n") ||
cell.contains("\r")
if needs_quotes {
sb.write_string("\"")
for i in 0.. Array[Double] raise HrvParseError {
let grid = parse_csv(content)
let result = []
for r in 0.. 0 {
try {
let val : Double = @strconv.from_str(trimmed)
result.push(val)
} catch {
_ => raise HrvParseError::InvalidNumber(input=cell)
}
}
}
}
if result.length() == 0 {
raise HrvParseError::EmptyInput
}
result
}
///|
/// Parse a CSV string containing historical morning measurements.
pub fn parse_morning_csv(
content : String,
) -> Array[MorningMeasurement] raise HrvParseError {
let grid = parse_csv(content)
if grid.length() <= 1 {
raise HrvParseError::EmptyInput
}
let mut start_idx = 0
let first_row = grid[0]
if first_row.length() > 0 {
let cell0 = trim(first_row[0])
if cell0.length() >= 4 && (cell0[0] == 100 || cell0[0] == 68) { // 'd' or 'D'
start_idx = 1
}
}
let result = []
for r in start_idx.. raise HrvParseError::InvalidNumber(input=row[1])
}
let sdnn = @strconv.from_str(trim(row[2])) catch {
_ => raise HrvParseError::InvalidNumber(input=row[2])
}
let rr_mean = @strconv.from_str(trim(row[3])) catch {
_ => raise HrvParseError::InvalidNumber(input=row[3])
}
result.push({ date, rmssd, sdnn, rr_mean })
}
result
}
///|
/// Serialize HrvMetrics to CSV string.
pub fn serialize_metrics_csv(metrics : HrvMetrics) -> String {
let grid = [
["metric", "value"],
["mean_rr", metrics.mean_rr.to_string()],
["mean_hr", metrics.mean_hr.to_string()],
["sdnn", metrics.sdnn.to_string()],
["rmssd", metrics.rmssd.to_string()],
["pnn50", metrics.pnn50.to_string()],
["pnn_custom", metrics.pnn_custom.to_string()],
["total_beats", metrics.quality.total_beats.to_string()],
["valid_beats", metrics.quality.valid_beats.to_string()],
["ectopic_beats", metrics.quality.ectopic_beats.to_string()],
["clean_ratio", metrics.quality.clean_ratio.to_string()],
["is_low_quality", metrics.quality.is_low_quality.to_string()],
]
to_csv(grid)
}
///|
/// Serialize morning baseline trends to CSV string.
pub fn serialize_trends_csv(trends : Array[MorningTrend]) -> String {
let grid = [
[
"date", "rmssd", "rolling_avg", "rolling_sd", "normal_lower", "normal_upper",
"status", "recovery_score",
],
]
for i in 0..