///|
/// Immutable source archive. Retains exact line endings, comments, duplicates,
/// section order and numeric spelling; construction is not a conformance claim.
pub struct LasDocument {
source : String
}
///|
pub(all) struct LasEntry {
section : String
line : Int
header : Header
}
///|
pub(all) struct LasDiagnostic {
kind : String
clause : String
section : String
line : Int
field : String
message : String
}
///|
/// Bounded capture, including invalid documents for diagnostics and repair.
pub fn read_las_document(text : String) -> LasDocument raise WellError {
if text.length() > 16000000 {
fail("resource: source exceeds 16000000 code units")
}
{ source: text, }
}
///|
/// Exact original text, never a regenerated interpretation of the source.
pub fn document_text(document : LasDocument) -> String {
document.source
}
///|
/// Ordered header entries, including duplicate names. Use check_las2 first if
/// conformance matters; an unparseable header is an error, never silently dropped.
pub fn document_entries(
document : LasDocument,
) -> Array[LasEntry] raise WellError {
let entries : Array[LasEntry] = []
let mut section = ""
let mut number = 0
for raw in document.source.split("\n") {
number += 1
let text = raw.trim().to_owned()
if text.has_prefix("~") && text.length() >= 2 {
section = text.iter().to_array()[1].to_string().to_upper()
} else if text != "" &&
!text.has_prefix("#") &&
["V", "W", "C", "P"].contains(section) {
entries.push({ section, line: number, header: parse_header(text), })
}
}
entries
}
///|
fn conformance_error(d : LasDiagnostic) -> String {
d.kind +
": LAS2 " +
d.clause +
" ~" +
d.section +
" line " +
d.line.to_string() +
" " +
d.field +
": " +
d.message
}
///|
/// Strict standard validation is separate from the unique-column finite-Double
/// analysis model. Legal duplicates/extreme numbers remain available in source.
pub fn document_to_log(document : LasDocument) -> Log raise WellError {
let diagnostics = check_las2(document)
if diagnostics.length() > 0 {
fail(conformance_error(diagnostics[0]))
}
try {
let state = strict_scan(document)
let null_entries = state.entries.filter(e => {
e.section == "W" && e.header.name == "NULL"
})
if !null_entries.is_empty() {
let entry = null_entries[0]
let exact_null = exact_decimal(entry.header.value)
let numeric_null = parse_number(entry.header.value)
for line in state.data {
for token in words(line.text) {
let numeric = parse_number(token)
if numeric == numeric_null &&
decimal_compare(exact_decimal(token), exact_null) != 0 {
fail(
"sample would collapse onto NULL after Double conversion at line " +
line.number.to_string(),
)
}
}
}
}
parse(document.source)
} catch {
WellError(message) => {
fail("model: valid LAS2 document cannot enter Log: " + message)
abort("unreachable")
}
}
}
///|
pub fn parse_strict(text : String) -> Log raise WellError {
document_to_log(read_las_document(text))
}
///|
/// Returns structured diagnostics; compatibility parse() never implies this check.
pub fn las2_report(document : LasDocument) -> Json {
let diagnostics = check_las2(document)
Json::object({
"baseline": Json::string("CWLS LAS 2.0 January 2014"),
"valid": Json::boolean(diagnostics.is_empty()),
"diagnostics": Json::array(
diagnostics.map(d => {
Json::object({
"kind": Json::string(d.kind),
"clause": Json::string(d.clause),
"section": Json::string(d.section),
"line": Json::number(d.line.to_double()),
"field": Json::string(d.field),
"message": Json::string(d.message),
})
}),
),
})
}