///|
priv enum TokenKind {
Absolute(Double)
Delta(Double)
Repeat(Int32)
Missing
}
///|
priv struct NumericToken {
kind : TokenKind
column : Int32
}
///|
priv struct DecodedLine {
values : Array[Double?]
uses_difference : Bool
first_ordinate_absolute : Bool
}
///|
fn separator(c : UInt16) -> Bool {
whitespace(c) || c == ',' || c == ';'
}
///|
fn absolute_prefix(c : UInt16) -> Int32? {
if c == '@' {
Some(0)
} else if c >= 'A' && c <= 'I' {
Some(c.to_int() - 64)
} else if c >= 'a' && c <= 'i' {
Some(96 - c.to_int())
} else {
None
}
}
///|
fn delta_prefix(c : UInt16) -> Int32? {
if c == '%' {
Some(0)
} else if c >= 'J' && c <= 'R' {
Some(c.to_int() - 73)
} else if c >= 'j' && c <= 'r' {
Some(105 - c.to_int())
} else {
None
}
}
///|
fn repeat_prefix(c : UInt16) -> Int32? {
if c >= 'S' && c <= 'Z' {
Some(c.to_int() - 82)
} else if c == 's' {
Some(9)
} else {
None
}
}
///|
fn coded_integer(
text : String,
start : Int32,
leading : Int32,
line : Int32,
) -> (Double, Int32) raise JcampError {
let mut value = leading.abs().to_double()
let mut i = start + 1
while i < text.length() && digit(text[i]) {
value = value * 10.0 + (text[i].to_int() - 48).to_double()
guard value <= 9007199254740991.0 else {
reject(
"ASDF_PRECISION",
"coded integer exceeds exact binary64 integer range",
line~,
column=start + 1,
)
}
i += 1
}
(if leading < 0 { -value } else { value }, i)
}
///|
fn numeric_tokens(
text : String,
mode : NumericMode,
options : ParseOptions,
line : Int32,
) -> Array[NumericToken] raise JcampError {
let tokens : Array[NumericToken] = []
let mut i = 0
while i < text.length() {
let c = text[i]
if separator(c) {
i += 1
continue
}
guard tokens.length() < options.limits.max_tokens_per_line else {
reject(
"LIMIT_TOKENS",
"numeric token budget exceeded",
line~,
column=i + 1,
)
}
let start = i
if c == '?' {
tokens.push({ kind: Missing, column: i + 1, })
i += 1
continue
}
if mode == Asdf {
if absolute_prefix(c) is Some(prefix) {
let (value, next) = coded_integer(text, i, prefix, line)
tokens.push({ kind: Absolute(value), column: i + 1, })
i = next
continue
}
if delta_prefix(c) is Some(prefix) {
let (value, next) = coded_integer(text, i, prefix, line)
tokens.push({ kind: Delta(value), column: i + 1, })
i = next
continue
}
if repeat_prefix(c) is Some(prefix) {
let (value, next) = coded_integer(text, i, prefix, line)
guard value <= options.limits.max_points.to_double() else {
reject(
"LIMIT_DUP",
"DUP count exceeds point budget",
line~,
column=i + 1,
)
}
tokens.push({ kind: Repeat(value.to_int()), column: i + 1, })
i = next
continue
}
}
if c == '+' || c == '-' {
i += 1
}
let mut digits = 0
while i < text.length() && digit(text[i]) {
i += 1
digits += 1
}
if i < text.length() && text[i] == '.' {
i += 1
while i < text.length() && digit(text[i]) {
i += 1
digits += 1
}
}
guard digits > 0 else {
reject(
"NUMERIC_TOKEN",
"unrecognized numeric token",
line~,
column=start + 1,
)
}
if mode != Asdf && i < text.length() && (text[i] == 'E' || text[i] == 'e') {
i += 1
if i < text.length() && (text[i] == '+' || text[i] == '-') {
i += 1
}
let exponent = i
while i < text.length() && digit(text[i]) {
i += 1
}
guard i > exponent else {
reject(
"NUMBER_EXPONENT",
"exponent has no digits",
line~,
column=start + 1,
)
}
}
let value = number(text[start:i].to_owned(), line~, column=start + 1)
tokens.push({ kind: Absolute(value), column: start + 1, })
}
tokens
}
///|
fn decode_line(
text : String,
options : ParseOptions,
line : Int32,
) -> DecodedLine raise JcampError {
let text = strip_comment(text)
let tokens = if options.numeric_mode == Auto {
numeric_tokens(text, Affn, options, line) catch {
Invalid(_) => numeric_tokens(text, Asdf, options, line)
}
} else {
numeric_tokens(text, options.numeric_mode, options, line)
}
let values : Array[Double?] = []
let mut previous : Double? = None
let mut previous_token : TokenKind? = None
let mut uses_difference = false
let first_ordinate_absolute = if tokens.length() >= 2 {
match tokens[1].kind {
Absolute(_) | Missing => true
_ => false
}
} else {
false
}
for token in tokens {
match token.kind {
Absolute(value) => {
guard values.length() < options.limits.max_points else {
reject(
"LIMIT_POINTS",
"expanded numeric row exceeds point budget",
line~,
column=token.column,
)
}
values.push(Some(value))
previous = Some(value)
previous_token = Some(token.kind)
}
Missing => {
guard values.length() < options.limits.max_points else {
reject(
"LIMIT_POINTS",
"expanded numeric row exceeds point budget",
line~,
column=token.column,
)
}
values.push(None)
previous = None
previous_token = None
}
Delta(delta) => {
guard previous is Some(value) else {
reject(
"DIF_ANCHOR",
"DIF requires a preceding finite absolute value",
line~,
column=token.column,
)
}
let next = value + delta
guard finite(next) && next.abs() <= 9007199254740991.0 else {
reject(
"ASDF_PRECISION",
"difference reconstruction exceeds exact integer range",
line~,
column=token.column,
)
}
guard values.length() < options.limits.max_points else {
reject(
"LIMIT_POINTS",
"expanded numeric row exceeds point budget",
line~,
column=token.column,
)
}
values.push(Some(next))
previous = Some(next)
previous_token = Some(token.kind)
uses_difference = true
}
Repeat(count) => {
guard previous_token is Some(operation) else {
reject(
"DUP_ANCHOR",
"DUP requires a preceding absolute or difference token",
line~,
column=token.column,
)
}
guard count >= 1 &&
count - 1 <= options.limits.max_points - values.length() else {
reject(
"LIMIT_DUP",
"DUP expansion exceeds remaining point budget",
line~,
column=token.column,
)
}
let mut repeat = 1
while repeat < count {
let value = match operation {
Absolute(value) => value
Delta(delta) => {
guard previous is Some(value) else {
reject(
"DIF_ANCHOR",
"missing difference anchor",
line~,
column=token.column,
)
}
let result = value + delta
guard finite(result) && result.abs() <= 9007199254740991.0 else {
reject(
"ASDF_PRECISION",
"DUP reconstruction exceeds exact integer range",
line~,
column=token.column,
)
}
result
}
_ =>
reject(
"DUP_ANCHOR",
"unsupported repeated token",
line~,
column=token.column,
)
}
values.push(Some(value))
previous = Some(value)
repeat += 1
}
}
}
}
{ values, uses_difference, first_ordinate_absolute, }
}
///|
pub fn decode_numbers(
text : String,
mode~ : NumericMode = Auto,
max_values~ : Int32 = 100000,
) -> Array[Double?] raise JcampError {
let options = {
..ParseOptions::default(),
numeric_mode: mode,
limits: { ..Limits::default(), max_points: max_values, },
}
check_options(options)
guard text.length() <= options.limits.max_line_chars else {
reject("LIMIT_LINE", "numeric row exceeds line budget")
}
decode_line(text, options, 1).values
}