///|
fn observation_json(observation : Observation) -> Json {
Json::object({
"timestamp": Json::number(observation.timestamp),
"distance": Json::number(observation.distance),
"elapsed": Json::number(observation.elapsed),
})
}
///|
fn object_number(object : Map[String, Json], field : String) -> Double? {
match object.get(field) {
Some(Json::Number(value, ..)) => Some(value)
_ => None
}
}
///|
fn parse_observation_json(
value : Json,
line : Int,
) -> ParseOutcome[Observation] {
match value {
Json::Object(object) => {
let timestamp = match object_number(object, "timestamp") {
None => return ParseFailed(MissingField(line~, field="timestamp"))
Some(value) => value
}
let distance = match object_number(object, "distance") {
None => return ParseFailed(MissingField(line~, field="distance"))
Some(value) => value
}
let elapsed = match object_number(object, "elapsed") {
None => return ParseFailed(MissingField(line~, field="elapsed"))
Some(value) => value
}
Parsed({ timestamp, distance, elapsed })
}
_ => ParseFailed(InvalidSyntax(line~, message="expected a JSON object"))
}
}
///|
/// Encode observations as one JSON object per line.
pub fn encode_json_lines(observations : Array[Observation]) -> String {
let builder = StringBuilder()
for observation in observations {
builder.write_string(observation_json(observation).stringify())
builder.write_char('\n')
}
builder.to_string()
}
///|
/// Parse JSON-line observations without aborting on malformed input.
pub fn parse_json_lines(input : String) -> ObservationParseResult {
if input.trim().is_empty() {
return parse_error_result(EmptyInput, 0)
}
let observations : Array[Observation] = []
let mut lines_read = 0
for raw in input.split("\n") {
let line = raw.trim().to_owned()
if !line.is_empty() {
lines_read += 1
let value = @json.parse(line) catch {
_ =>
return parse_error_result(
InvalidSyntax(line=lines_read, message="invalid JSON"),
lines_read,
)
}
let parsed = parse_observation_json(value, lines_read)
match parsed {
ParseFailed(error) => return parse_error_result(error, lines_read)
Parsed(observation) => observations.push(observation)
}
}
}
{
observations: normalize_observations(observations),
error: None,
lines_read,
}
}
///|
/// Encode a dataset with an optional target field.
pub fn encode_dataset_json_lines(dataset : ObservationDataset) -> String {
let builder = StringBuilder()
for row in dataset.rows {
let object : Map[String, Json] = {
"timestamp": Json::number(row.observation.timestamp),
"distance": Json::number(row.observation.distance),
"elapsed": Json::number(row.observation.elapsed),
"weight": Json::number(row.weight),
}
match row.actual_remaining {
None => ()
Some(value) => object["actual_remaining"] = Json::number(value)
}
builder.write_string(Json::object(object).stringify())
builder.write_char('\n')
}
builder.to_string()
}
///|
/// Parse JSON lines that optionally contain actual_remaining and weight.
pub fn parse_json_dataset(
input : String,
name? : String = "json",
) -> ParseOutcome[ObservationDataset] {
let parsed = parse_json_lines(input)
match parsed.error {
Some(error) => ParseFailed(error)
None => {
let rows : Array[DatasetRow] = []
for observation in parsed.observations {
rows.push({ observation, actual_remaining: None, weight: 1.0 })
}
Parsed({ rows, name })
}
}
}