// CSV Reader -- Parse comma-separated data with headers
//
// Supports newline-delimited rows with comma-separated fields.
// First row is treated as headers; remaining rows are data.
// Uses MoonBit's built-in String.split() for simplicity.
// Parse CSV text into (headers, rows)
///|
pub fn parse_csv(content : String) -> (Array[String], Array[Array[String]]) {
let line_views = content.split("\n").to_array()
// Collect non-empty lines
let lines : Array[String] = []
for i = 0; i < line_views.length(); i = i + 1 {
let line = line_views[i].to_owned()
if line != "" {
lines.push(line)
}
}
if lines.length() == 0 {
([], [])
} else {
// First line = headers
let hdr_views = lines[0].split(",").to_array()
let headers : Array[String] = []
for i = 0; i < hdr_views.length(); i = i + 1 {
headers.push(hdr_views[i].to_owned())
}
// Remaining lines = data rows
let rows : Array[Array[String]] = []
for i = 1; i < lines.length(); i = i + 1 {
let fld_views = lines[i].split(",").to_array()
let fields : Array[String] = []
for j = 0; j < fld_views.length(); j = j + 1 {
fields.push(fld_views[j].to_owned())
}
rows.push(fields)
}
(headers, rows)
}
}
// Parse CSV with numeric values, returning labels and Series objects.
// Headers: "Label,Series1Name,Series2Name,..."
// First column = category labels, remaining columns = data series.
///|
pub fn parse_csv_to_series(content : String) -> (Array[String], Array[Series]) {
let (headers, rows) = parse_csv(content)
let num_series = if headers.length() > 1 { headers.length() - 1 } else { 0 }
if num_series == 0 || rows.length() == 0 {
([], [])
} else {
let series_list : Array[Series] = []
for si = 0; si < num_series; si = si + 1 {
let values : Array[Float] = []
for ri = 0; ri < rows.length(); ri = ri + 1 {
let row = rows[ri]
let col_idx = si + 1
if col_idx < row.length() {
values.push(parse_float_or_zero(row[col_idx]))
} else {
values.push(0.0)
}
}
series_list.push(Series::new(headers[si + 1], values))
}
let labels : Array[String] = []
for ri = 0; ri < rows.length(); ri = ri + 1 {
labels.push(rows[ri][0])
}
(labels, series_list)
}
}
// Parse a string to Float. Handles integers, decimals (e.g. "3.14"),
// and negative numbers (e.g. "-7"). Returns 0.0 on failure.
///|
pub fn parse_float_or_zero(s : String) -> Float {
let len = s.length()
if len == 0 {
Float::from_int(0)
} else {
parse_float_impl(s, 0, len)
}
}
///|
fn parse_float_impl(s : String, i : Int, len : Int) -> Float {
// Check for negative sign
let pair : (Float, Int) = if i < len && s[i] == 45 {
(Float::from_int(-1), i + 1)
} else {
(Float::from_int(1), i)
}
let sign = pair.0
let start = pair.1
let (int_val, after_int) = parse_int_digits(s, start, len, Float::from_int(0))
if after_int < len && s[after_int] == 46 {
let place_val : Float = Float::from_int(1) / Float::from_int(10)
let (frac_val, _) = parse_frac_digits(
s,
after_int + 1,
len,
Float::from_int(0),
place_val,
)
sign * (int_val + frac_val)
} else {
sign * int_val
}
}
// Parse a sequence of digit characters into a Float value.
// Returns (accumulated value, next index).
///|
fn parse_int_digits(
s : String,
i : Int,
len : Int,
acc : Float,
) -> (Float, Int) {
if i >= len {
(acc, i)
} else {
let ch = s[i] // UInt16 character code
if ch >= 48 && ch <= 57 {
let digit = code_to_float(ch)
parse_int_digits(s, i + 1, len, acc * Float::from_int(10) + digit)
} else {
(acc, i)
}
}
}
// Parse fractional digits after decimal point.
// Returns (accumulated fraction, next index).
///|
fn parse_frac_digits(
s : String,
i : Int,
len : Int,
acc : Float,
place : Float,
) -> (Float, Int) {
if i >= len {
(acc, i)
} else {
let ch = s[i]
if ch >= 48 && ch <= 57 {
let digit = code_to_float(ch)
let new_place : Float = place / Float::from_int(10)
parse_frac_digits(s, i + 1, len, acc + digit * place, new_place)
} else {
(acc, i)
}
}
}
// Convert UInt16 character code for '0'-'9' to Float value.
///|
fn code_to_float(code : UInt16) -> Float {
if code == 48 {
Float::from_int(0)
} else if code == 49 {
Float::from_int(1)
} else if code == 50 {
Float::from_int(2)
} else if code == 51 {
Float::from_int(3)
} else if code == 52 {
Float::from_int(4)
} else if code == 53 {
Float::from_int(5)
} else if code == 54 {
Float::from_int(6)
} else if code == 55 {
Float::from_int(7)
} else if code == 56 {
Float::from_int(8)
} else if code == 57 {
Float::from_int(9)
} else {
Float::from_int(0)
}
}