///|
/// Numeric CSV dialect: comma delimiter, optional first header, LF/CRLF.
/// Quotes, missing cells and blank records are rejected; one trailing newline is allowed.
pub fn trajectory_csv(
  text : String,
  header : Bool,
) -> Result[Trajectory, RqaError] {
  if text.length() > 2097152 {
    return Err(Limit("CSV text size"))
  }
  let lines = text.split("\n").to_array()
  if lines.length() > 0 && lines[lines.length() - 1].length() == 0 {
    ignore(lines.pop())
  }
  let first = if header { 1 } else { 0 }
  if lines.length() <= first || lines.length() - first > 4096 {
    return Err(Limit("CSV data rows"))
  }
  let rows : Array[Array[Double]] = []
  let mut width = 0
  if header {
    let fields = lines[0].trim().split(",").to_array()
    width = fields.length()
    for field in fields {
      let clean = field.trim()
      if clean.length() == 0 || clean.contains("\"") {
        return Err(Invalid("CSV header field"))
      }
    }
  }
  for line in first.. 64 || fields.length() != width {
      return Err(Invalid("CSV width at line \{line+1}"))
    }
    let row : Array[Double] = []
    for field in fields {
      let token = field.trim()
      if token.length() == 0 || token.contains("\"") {
        return Err(Invalid("CSV numeric field at line \{line+1}"))
      }
      let value = @string.parse_double(token) catch {
        _ => return Err(Invalid("CSV number at line \{line+1}"))
      }
      if !valid_number(value) {
        return Err(Numeric("CSV coordinate at line \{line+1}"))
      }
      row.push(value)
    }
    rows.push(row)
  }
  trajectory(rows)
}

///|
/// Round-trip numeric CSV without a header. Double formatting is locale independent.
pub fn Trajectory::to_csv(self : Trajectory) -> String {
  let out = StringBuilder()
  for i in 0.. 0 {
        out.write_string(",")
      }
      out.write_string(self.values[i * self.dimensions + k].to_string())
    }
    out.write_string("\n")
  }
  out.to_string()
}