///|
fn whitespace(c : UInt16) -> Bool {
c == ' ' || c == '\t' || c == '\r' || c == '\n'
}
///|
fn digit(c : UInt16) -> Bool {
c >= '0' && c <= '9'
}
///|
fn trim(text : String) -> String {
let mut first = 0
let mut last = text.length()
while first < last && whitespace(text[first]) {
first += 1
}
while last > first && whitespace(text[last - 1]) {
last -= 1
}
text[first:last].to_owned()
}
///|
pub fn canonical_label(label : String) -> String {
let out = StringBuilder()
for c in label.iter() {
if c != ' ' && c != '\t' && c != '-' && c != '_' {
out.write_char(c)
}
}
out.to_string()[:].to_upper().to_owned()
}
///|
pub fn record_value(records : Array[Record], label : String) -> String? {
let key = canonical_label(label)
for record in records {
if canonical_label(record.label) == key {
return Some(record.value)
}
}
None
}
///|
pub fn set_record(
records : Array[Record],
label : String,
value : String,
) -> Array[Record] {
let key = canonical_label(label)
let result : Array[Record] = []
let mut replaced = false
for record in records {
if canonical_label(record.label) == key {
if !replaced {
result.push({ label: record.label, value, line: record.line, })
replaced = true
}
} else {
result.push(record)
}
}
if !replaced {
result.push({ label, value, line: 0, })
}
result
}
///|
fn strip_comment(text : String) -> String {
match text.find("$$") {
Some(index) => text[:index].to_owned()
None => text
}
}
///|
fn physical_lines(
text : String,
options : ParseOptions,
) -> Array[DataLine] raise JcampError {
check_options(options)
guard text.length() <= options.limits.max_chars else {
reject("LIMIT_TEXT", "input exceeds text budget")
}
guard !text.contains("\u0000") else {
reject("TEXT_NUL", "NUL is not permitted in JCAMP text")
}
let text = if text.has_prefix("\uFEFF") { text[1:].to_owned() } else { text }
let text = text
.replace_all(old="\r\n", new="\n")
.replace_all(old="\r", new="\n")
let result : Array[DataLine] = []
for value in text.split("\n") {
let line = result.length() + 1
guard line <= options.limits.max_lines else {
reject("LIMIT_LINES", "too many physical lines", line~)
}
guard value.length() <= options.limits.max_line_chars else {
reject("LIMIT_LINE", "physical line exceeds budget", line~)
}
result.push({ text: value.to_owned(), line, })
}
result
}
///|
fn number(
text : String,
line~ : Int32 = 0,
column~ : Int32 = 1,
) -> Double raise JcampError {
let text = trim(text)
guard text != "" else {
reject("NUMBER_EMPTY", "expected a number", line~, column~)
}
let mut i = 0
if text[i] == '+' || text[i] == '-' {
i += 1
}
let mut digits = 0
while i < text.length() && digit(text[i]) {
digits += 1
i += 1
}
if i < text.length() && text[i] == '.' {
i += 1
while i < text.length() && digit(text[i]) {
digits += 1
i += 1
}
}
guard digits > 0 else {
reject("NUMBER_SYNTAX", "number has no digits", line~, column~)
}
if i < text.length() && (text[i] == 'E' || text[i] == 'e') {
i += 1
if i < text.length() && (text[i] == '+' || text[i] == '-') {
i += 1
}
let start = i
while i < text.length() && digit(text[i]) {
i += 1
}
guard i > start else {
reject("NUMBER_EXPONENT", "exponent has no digits", line~, column~)
}
}
guard i == text.length() else {
reject("NUMBER_SYNTAX", "invalid numeric characters", line~, column~)
}
let value = @string.parse_double(text[:]) catch {
_ => reject("NUMBER_RANGE", "numeric conversion failed", line~, column~)
}
guard finite(value) else {
reject("NUMBER_RANGE", "non-finite numeric value", line~, column~)
}
value
}
///|
fn required_number(
records : Array[Record],
key : String,
line : Int32,
) -> Double raise JcampError {
match record_value(records, key) {
Some(value) => number(value, line~)
None => reject("HEADER_MISSING", "missing " + key, line~)
}
}
///|
fn optional_number(
records : Array[Record],
key : String,
default : Double,
line : Int32,
) -> Double raise JcampError {
match record_value(records, key) {
Some(value) => number(value, line~)
None => default
}
}
///|
fn point_count(
records : Array[Record],
options : ParseOptions,
line : Int32,
) -> Int32 raise JcampError {
let count = required_number(records, "NPOINTS", line)
guard count >= 1.0 &&
count.floor() == count &&
count <= options.limits.max_points.to_double() else {
reject(
"POINT_COUNT",
"NPOINTS must be a positive integer within the point budget",
line~,
)
}
count.to_int()
}