///|
/// A portable text representation for exchanging waveform samples.
pub struct TextWaveform {
station : String
channel : String
sample_rate : Double
start_time : Double
data : Array[Double]
}
///|
/// Construct a text waveform value.
pub fn TextWaveform::new(
station : String,
channel : String,
sample_rate : Double,
start_time : Double,
data : Array[Double],
) -> TextWaveform {
{ station, channel, sample_rate, start_time, data }
}
///|
/// Parse a single numeric token with a useful column error.
fn parse_text_number(
token : String,
line_number : Int,
) -> Result[Double, String] {
Ok(@strconv.from_str(token.to_string_view())) catch {
_ => Err("Invalid waveform number at line \{line_number}: \{token}")
}
}
///|
/// Parse whitespace-separated samples, ignoring blank and comment lines.
pub fn parse_ascii_samples(text : String) -> Result[Array[Double], String] {
let result = Array::new()
let mut line_number = 0
for line_view in text.split("\n") {
line_number = line_number + 1
let line = line_view.trim().to_owned()
if line == "" || line.has_prefix("#") {
continue
}
for token_view in line.split(" ") {
let token = token_view.trim().to_owned()
if token != "" && token != "\r" {
match parse_text_number(token, line_number) {
Ok(value) => result.push(value)
Err(message) => return Err(message)
}
}
}
}
Ok(result)
}
///|
/// Parse comma-separated samples with an optional header row.
pub fn parse_csv_samples(
text : String,
has_header : Bool,
) -> Result[Array[Double], String] {
let result = Array::new()
let mut line_number = 0
for line_view in text.split("\n") {
line_number = line_number + 1
if has_header && line_number == 1 {
continue
}
let line = line_view.trim().to_owned()
if line == "" || line.has_prefix("#") {
continue
}
let fields = line.split(",")
for field_view in fields {
let token = field_view.trim().to_owned()
match parse_text_number(token, line_number) {
Ok(value) => result.push(value)
Err(message) => return Err(message)
}
}
}
Ok(result)
}
///|
/// Serialize samples as one value per line.
pub fn samples_to_ascii(
data : Array[Double],
precision_label : String,
) -> String {
let mut output = "# moonbit-seismic waveform ".add(precision_label).add("\n")
for value in data {
output = output + value.to_string() + "\n"
}
output
}
///|
/// Serialize samples as a CSV column with an explicit header.
pub fn samples_to_csv(data : Array[Double], header : String) -> String {
let mut output = header + "\n"
for value in data {
output = output + value.to_string() + "\n"
}
output
}
///|
/// Parse a metadata-prefixed CSV waveform.
///
/// The first four rows are `station`, `channel`, `sample_rate`, and
/// `start_time`; remaining rows contain one sample each.
pub fn parse_text_waveform(text : String) -> Result[TextWaveform, String] {
let lines = Array::new()
for line_view in text.split("\n") {
let line = line_view.trim().to_owned()
if line != "" && !line.has_prefix("#") {
lines.push(line)
}
}
if lines.length() < 4 {
return Err("Text waveform requires four metadata lines")
}
let station = lines[0]
let channel = lines[1]
let sample_rate = match parse_text_number(lines[2], 3) {
Ok(value) => value
Err(message) => return Err(message)
}
let start_time = match parse_text_number(lines[3], 4) {
Ok(value) => value
Err(message) => return Err(message)
}
let data = Array::new()
for index = 4; index < lines.length(); index = index + 1 {
match parse_text_number(lines[index], index + 1) {
Ok(value) => data.push(value)
Err(message) => return Err(message)
}
}
Ok({ station, channel, sample_rate, start_time, data })
}
///|
/// Serialize a waveform in the portable metadata-prefixed text format.
pub fn text_waveform_to_string(waveform : TextWaveform) -> String {
let mut output = waveform.station + "\n" + waveform.channel + "\n"
output = output + waveform.sample_rate.to_string() + "\n"
output = output + waveform.start_time.to_string() + "\n"
for value in waveform.data {
output = output + value.to_string() + "\n"
}
output
}
///|
/// Convert a trace to the text exchange type.
pub fn trace_to_text_waveform(trace : Trace) -> TextWaveform {
TextWaveform::new(
trace.station,
trace.channel,
trace.sampling_rate,
trace.start_time,
trace.data.copy(),
)
}
///|
/// Convert a text exchange value to a trace.
pub fn text_waveform_to_trace(waveform : TextWaveform) -> Trace {
Trace::new(
waveform.station,
waveform.channel,
waveform.sample_rate,
waveform.start_time,
waveform.data,
)
}
///|
/// Serialize a trace as a two-column CSV of time and amplitude.
pub fn trace_to_csv(trace : Trace) -> String {
let mut output = "time,amplitude\n"
for index = 0; index < trace.npts(); index = index + 1 {
let time = trace.start_time + index.to_double() * trace.delta()
output = output +
time.to_string() +
"," +
trace.data[index].to_string() +
"\n"
}
output
}
///|
/// Parse a two-column time/amplitude CSV and infer sample rate.
pub fn parse_time_amplitude_csv(
text : String,
station : String,
channel : String,
) -> Result[Trace, String] {
let times = Array::new()
let values = Array::new()
let mut line_number = 0
for line_view in text.split("\n") {
line_number = line_number + 1
let line = line_view.trim().to_owned()
if line == "" || line.has_prefix("#") || line.has_prefix("time,") {
continue
}
let fields = Array::new()
for field_view in line.split(",") {
fields.push(field_view.trim().to_owned())
}
if fields.length() < 2 {
return Err("Expected time and amplitude at line {line_number}")
}
let time = match parse_text_number(fields[0], line_number) {
Ok(value) => value
Err(message) => return Err(message)
}
let value = match parse_text_number(fields[1], line_number) {
Ok(value) => value
Err(message) => return Err(message)
}
times.push(time)
values.push(value)
}
if times.length() < 2 {
return Err("Time/amplitude CSV requires at least two rows")
}
let dt = times[1] - times[0]
if dt <= 0.0 {
return Err("CSV times must be strictly increasing")
}
for index = 2; index < times.length(); index = index + 1 {
if (times[index] - times[index - 1] - dt).abs() > dt * 0.01 {
return Err("CSV time steps must be uniform")
}
}
Ok(Trace::new(station, channel, 1.0 / dt, times[0], values))
}