///|
/// Invalid input, unsupported format or unsafe operation. Messages include context.
pub(all) suberror WellError {
WellError(String)
}
///|
pub(all) struct Curve {
api_code : String
name : String
unit : String
description : String
}
///|
pub(all) struct Header {
name : String
unit : String
value : String
description : String
}
///|
/// An uninterpreted customer section. Title includes the leading tilde.
pub(all) struct LasSection {
title : String
lines : Array[String]
}
///|
/// A rectangular table; column zero is non-null strictly monotonic depth.
/// Arrays are owned snapshots at construction. Call validate after external mutation.
pub struct Log {
curves : Array[Curve]
rows : Array[Array[Double?]]
headers : Array[Header]
parameters : Array[Header]
other : Array[String]
version_headers : Array[Header]
extra_sections : Array[LasSection]
null_value : Double
}
///|
fn fail(message : String) -> Unit raise WellError {
raise WellError(message)
}
///|
fn finite(value : Double) -> Bool {
!value.is_nan() && !value.is_inf()
}
///|
fn canonical(text : String) -> String {
text.trim().to_owned().to_upper()
}
///|
fn safe_text(text : String) -> Bool {
!text.contains("\n") && !text.contains("\r")
}
///|
/// LAS 2.0 section 5.2: printable ASCII except internal spaces, dots and colons.
fn valid_name(name : String) -> Bool {
if name.length() == 0 || name.has_prefix("#") || name.has_prefix("~") {
return false
}
for c in name.iter() {
let n = c.to_int()
if n < 33 || n > 126 || c == '.' || c == ':' {
return false
}
}
true
}
///|
pub fn make_log(
curves : Array[Curve],
rows : Array[Array[Double?]],
headers? : Array[Header] = [],
parameters? : Array[Header] = [],
other? : Array[String] = [],
null_value? : Double = -999.25,
version_headers? : Array[Header] = [],
extra_sections? : Array[LasSection] = [],
) -> Log raise WellError {
let log = Log::{
curves: curves.copy(),
rows: rows.map(row => row.copy()),
headers: headers.copy(),
parameters: parameters.copy(),
other: other.copy(),
version_headers: version_headers.copy(),
extra_sections: extra_sections.map(s => {
title: s.title,
lines: s.lines.copy(),
}),
null_value,
}
validate(log)
log
}
///|
pub fn validate(log : Log) -> Unit raise WellError {
if log.curves.length() == 0 {
fail("at least one depth curve is required")
}
if log.curves.length() > 512 {
fail("curve limit is 512")
}
if log.rows.length() > 1000000 {
fail("row limit is 1000000")
}
if log.rows.length().to_double() * log.curves.length().to_double() >
10000000.0 {
fail("log cell budget is 10000000")
}
if !finite(log.null_value) {
fail("null sentinel must be finite")
}
for i in 0.. c.to_int() < 32 || c.to_int() > 126) {
fail("unsafe curve metadata: " + c.name)
}
for j in 0.. v
None => {
fail("missing depth at row " + r.to_string())
0.0
}
}
for value in row {
if value is Some(v) && !finite(v) {
fail("nonfinite sample at row " + r.to_string())
}
}
if r > 0 {
if depth == previous {
fail("duplicate depth at row " + r.to_string())
}
let sign = if depth > previous { 1 } else { -1 }
if direction != 0 && sign != direction {
fail("depth reversal at row " + r.to_string())
}
direction = sign
}
previous = depth
}
let all_headers = log.headers.copy()
for h in log.parameters {
all_headers.push(h)
}
for h in log.version_headers {
let key = canonical(h.name)
if key == "VERS" || key == "WRAP" {
fail("version extensions cannot override VERS or WRAP")
}
all_headers.push(h)
}
for h in all_headers {
if !valid_name(h.name) ||
!safe_text(h.value) ||
!safe_text(h.unit) ||
h.unit.contains(" ") ||
h.unit.contains("\t") ||
h.unit.contains(":") ||
!safe_text(h.description) ||
h.description.contains(":") {
fail("unsafe header: " + h.name)
}
}
for collection in [log.headers, log.parameters, log.version_headers] {
for i in 0.. Int raise WellError {
for i in 0.. Array[Double] raise WellError {
validate(log)
log.rows.map(row => row[0].unwrap())
}