///|
/// Parsed command-line options kept private to the executable package.
struct CliOptions {
action : String
format : String
cleaning : String
data : String
sample_rate : Double
repetitions : Int
}
///|
suberror CliError {
InvalidData
}
///|
fn print_usage() -> Unit {
println("moonbit-hrvkit ")
println(" --action ")
println(" --format Output format (default: csv)")
println(" --cleaning ")
println(" --sample-rate Tachogram rate for report (default: 4)")
println(" --repetitions Benchmark repetitions (default: 10)")
println(" --data CSV or JSON input")
}
///|
fn parse_cli_options(args : Array[String]) -> CliOptions {
let mut action = ""
let mut format = "csv"
let mut cleaning = "remove"
let mut data = ""
let mut sample_rate = 4.0
let mut repetitions = 10
let mut i = 1
while i < args.length() {
if args[i] == "--action" && i + 1 < args.length() {
action = args[i + 1]
i += 2
} else if args[i] == "--format" && i + 1 < args.length() {
format = args[i + 1]
i += 2
} else if args[i] == "--cleaning" && i + 1 < args.length() {
cleaning = args[i + 1]
i += 2
} else if args[i] == "--data" && i + 1 < args.length() {
data = args[i + 1]
i += 2
} else if args[i] == "--sample-rate" && i + 1 < args.length() {
sample_rate = @strconv.from_str(args[i + 1]) catch { _ => 4.0 }
i += 2
} else if args[i] == "--repetitions" && i + 1 < args.length() {
repetitions = @strconv.from_str(args[i + 1]) catch { _ => 10 }
i += 2
} else {
i += 1
}
}
{ action, format, cleaning, data, sample_rate, repetitions }
}
///|
fn cleaning_method(name : String) -> @hrvkit.CleaningMethod {
match name {
"median" => @hrvkit.InterpolateLocalMedian
"linear" => @hrvkit.InterpolateLinear
_ => @hrvkit.Remove
}
}
///|
fn parse_intervals(options : CliOptions) -> Array[Double] raise CliError {
if options.format == "json" {
try {
let json_data = @json.parse(options.data)
@json.from_json(json_data)
} catch {
_ => raise CliError::InvalidData
}
} else {
@hrvkit.parse_rr_csv(options.data) catch {
_ => raise CliError::InvalidData
}
}
}
///|
fn emit_clean(options : CliOptions, config : @hrvkit.HrvConfig) -> Unit {
try {
let intervals = parse_intervals(options)
let (cleaned, quality) = @hrvkit.clean_rr_intervals(
intervals,
cleaning_method(options.cleaning),
config,
)
if options.format == "json" {
let output : Map[String, Json] = Default::default()
output.set("cleaned", cleaned.to_json())
output.set("quality", quality.to_json())
println(Json::object(output).stringify())
} else {
let row = []
for value in cleaned {
row.push(value.to_string())
}
println(@hrvkit.to_csv([row]))
}
} catch {
_ => println("error: invalid interval data")
}
}
///|
fn emit_metrics(options : CliOptions, config : @hrvkit.HrvConfig) -> Unit {
try {
let intervals = parse_intervals(options)
let (cleaned, quality) = @hrvkit.clean_rr_intervals(
intervals,
cleaning_method(options.cleaning),
config,
)
let metrics = @hrvkit.calculate_metrics(cleaned, config, quality)
if options.format == "json" {
println(metrics.to_json().stringify())
} else {
println(@hrvkit.serialize_metrics_csv(metrics))
}
} catch {
_ => println("error: invalid interval data")
}
}
///|
fn emit_trends(options : CliOptions, config : @hrvkit.HrvConfig) -> Unit {
try {
let history : Array[@hrvkit.MorningMeasurement] = if options.format ==
"json" {
@json.from_json(@json.parse(options.data))
} else {
@hrvkit.parse_morning_csv(options.data)
}
let trends = @hrvkit.calculate_morning_trends(
history,
config.rolling_window,
config,
)
if options.format == "json" {
println(trends.to_json().stringify())
} else {
println(@hrvkit.serialize_trends_csv(trends))
}
} catch {
_ => println("error: invalid morning measurement data")
}
}
///|
fn emit_report(options : CliOptions, config : @hrvkit.HrvConfig) -> Unit {
try {
let intervals = parse_intervals(options)
let defaults = @hrvkit.AnalysisOptions::default()
let report_options : @hrvkit.AnalysisOptions = {
cleaning_method: cleaning_method(options.cleaning),
sample_rate_hz: options.sample_rate,
remove_trend: defaults.remove_trend,
window_function: defaults.window_function,
segment_size: defaults.segment_size,
segment_hop: defaults.segment_hop,
nonlinear_enabled: defaults.nonlinear_enabled,
}
let report = @hrvkit.analyze_rr(intervals, config, report_options)
if options.format == "json" {
println(report.to_json().stringify())
} else {
println(@hrvkit.report_headline_csv(report))
println(@hrvkit.report_summary(report))
}
} catch {
_ => println("error: invalid interval data")
}
}
///|
fn emit_quality(options : CliOptions, config : @hrvkit.HrvConfig) -> Unit {
try {
let intervals = parse_intervals(options)
let diagnostic = @hrvkit.diagnose_signal(intervals, config)
if options.format == "json" {
println(diagnostic.to_json().stringify())
} else {
println(@hrvkit.diagnostic_csv(diagnostic))
println(@hrvkit.diagnostic_recommendation(diagnostic))
}
} catch {
_ => println("error: invalid interval data")
}
}
///|
fn emit_benchmark(options : CliOptions, _config : @hrvkit.HrvConfig) -> Unit {
let repetitions = if options.repetitions < 1 {
1
} else {
options.repetitions
}
let benchmark_config : @hrvkit.BenchmarkConfig = {
recording_length: 256,
repetitions,
baseline_rr: 800.0,
sample_rate_hz: options.sample_rate,
}
let summary = @hrvkit.run_benchmark(benchmark_config)
if options.format == "json" {
println(summary.to_json().stringify())
} else {
println(@hrvkit.benchmark_csv(summary))
}
}
///|
fn main {
let options = parse_cli_options(@env.args())
if options.action == "" ||
options.action == "help" ||
options.action == "--help" {
print_usage()
return
}
if options.action == "version" {
println("moonbit-hrvkit 0.3.0")
return
}
let config = @hrvkit.HrvConfig::default()
match options.action {
"clean" => emit_clean(options, config)
"metrics" => emit_metrics(options, config)
"trends" => emit_trends(options, config)
"report" => emit_report(options, config)
"quality" => emit_quality(options, config)
"benchmark" => emit_benchmark(options, config)
_ => print_usage()
}
}