///|
fn parameter_key(key : String) -> (Int, String)? raise FcsError {
let k = key.to_upper()
if !k.has_prefix("$P") || k.length() < 4 {
return None
}
let mut i = 2
while i < k.length() && k[i].to_int() >= 48 && k[i].to_int() <= 57 {
i = i + 1
}
if i == 2 || i == k.length() {
return None
}
Some((natural(k[2:i].to_owned()), k[i:].to_owned()))
}
///|
fn padded(n : Int, width : Int) -> String raise FcsError {
let s = n.to_string()
if n < 0 || s.length() > width {
raise Limit("offset field overflow")
}
String::make(width - s.length(), '0') + s
}
///|
fn assemble(
meta : Array[(String, String)],
raw : Bytes,
analysis : Array[(String, String)],
) -> Bytes raise FcsError {
let a = if analysis.is_empty() { b"" } else { encode_text(analysis) }
put(meta, "$BEGINSTEXT", "0")
put(meta, "$ENDSTEXT", "0")
put(meta, "$NEXTDATA", "0")
for key in ["$BEGINDATA", "$ENDDATA", "$BEGINANALYSIS", "$ENDANALYSIS"] {
put(meta, key, "0000000000")
}
let size = encode_text(meta).length()
let start = 256 + size
let stop = start + raw.length() - 1
let ast = if a.is_empty() { 0 } else { stop + 1 }
let ae = if a.is_empty() { 0 } else { ast + a.length() - 1 }
put(meta, "$BEGINDATA", padded(start, 10))
put(meta, "$ENDDATA", padded(stop, 10))
put(meta, "$BEGINANALYSIS", padded(ast, 10))
put(meta, "$ENDANALYSIS", padded(ae, 10))
let txt = encode_text(meta)
if txt.length() != size {
raise Invalid("internal TEXT layout mismatch")
}
let hdrfield = fn(n) raise FcsError {
padded(if n <= 99999999 { n } else { 0 }, 8)
}
let hdr = @utf8.encode(
"FCS3.1 " +
padded(256, 8) +
padded(255 + size, 8) +
hdrfield(start) +
hdrfield(stop) +
hdrfield(ast) +
hdrfield(ae),
)
let total = start + raw.length() + a.length()
if total > 268435456 {
raise Limit("output exceeds 256 MiB")
}
Bytes::makei(total, i => {
if i < 58 {
hdr[i]
} else if i < 256 {
b' '
} else if i < start {
txt[i - 256]
} else if i <= stop {
raw[i - start]
} else {
a[i - ast]
}
})
}
///|
fn edits(
meta : Array[(String, String)],
updates : Array[(String, String)],
) -> Unit raise FcsError {
let reserved = [
"$BEGINDATA", "$ENDDATA", "$BEGINANALYSIS", "$ENDANALYSIS", "$BEGINSTEXT", "$ENDSTEXT",
"$NEXTDATA", "$BYTEORD", "$DATATYPE", "$MODE", "$PAR", "$TOT", "$SPILL", "$SPILLOVER",
"$COMP", "$CRC", "$ORIGINALITY",
]
for (k, v) in updates {
if reserved.contains(k.to_upper()) || parameter_key(k) is Some(_) {
raise Invalid("structural keyword cannot be edited: \{k}")
}
put(meta, k, v)
}
}
///|
/// Creates a standalone canonical FCS 3.1 dataset. Event bytes are not decoded/re-encoded.
/// Changing events/channels with ANALYSIS requires explicit drop_analysis=true.
pub fn Dataset::write(
self : Dataset,
event_indices? : Array[Int],
channel_indices? : Array[Int],
updates? : Array[(String, String)] = [],
drop_spillover? : Bool = false,
drop_analysis? : Bool = false,
) -> Bytes raise FcsError {
let es = event_indices.unwrap_or_else(() => Array::makei(self.events, i => i))
let cs = channel_indices.unwrap_or_else(() => {
Array::makei(self.channels.length(), i => i)
})
if cs.is_empty() ||
cs.length() > self.channels.length() ||
es.length() > 8000000 / cs.length() {
raise Limit("output event/channel count")
}
let seen : Map[Int, Bool] = Map([])
for c in cs {
self.check_channel(c)
if seen.contains(c) {
raise Invalid("duplicate output channel")
}
seen[c] = true
}
for e in es {
if e < 0 || e >= self.events {
raise Invalid("selected event out of bounds")
}
}
let changed_channels = cs.length() != self.channels.length() ||
cs.mapi((i, c) => i != c).contains(true)
let changed_events = es.length() != self.events ||
es.mapi((i, e) => i != e).contains(true)
if (changed_channels || changed_events) &&
!self.analysis.is_empty() &&
!drop_analysis {
raise Invalid(
"selection may invalidate ANALYSIS; explicitly request drop_analysis",
)
}
if changed_channels && self.keyword("$COMP") != None && !drop_spillover {
raise Invalid(
"legacy COMP requires explicit removal before channel changes",
)
}
if self.spillover() is Some(s) && !drop_spillover {
for name in s.channels {
if !cs.map(i => self.channels[i].name).contains(name) {
raise Invalid(
"selection removes spillover channel; explicitly request drop_spillover",
)
}
}
}
let meta = []
for (key, value) in self.metadata {
let k = key.to_upper()
if k == "$CRC" ||
k == "$UNICODE" ||
(drop_spillover && ["$SPILL", "$SPILLOVER", "$COMP"].contains(k)) {
continue
}
match parameter_key(k) {
Some((old, suffix)) =>
for i = 0; i < cs.length(); i = i + 1 {
if cs[i] == old - 1 {
meta.push(("$P\{i + 1}\{suffix}", value))
}
}
None => meta.push((k, value))
}
}
// Normalize only required layout/amplification fields. Unknown metadata is retained.
for i = 0; i < cs.length(); i = i + 1 {
let c = self.channels[cs[i]]
put(meta, "$P\{i+1}E", "\{c.decades},\{c.zero}")
}
put(
meta,
"$BYTEORD",
if self.order == LittleEndian {
"1,2,3,4"
} else {
"4,3,2,1"
},
)
put(meta, "$PAR", cs.length().to_string())
put(meta, "$TOT", es.length().to_string())
put(meta, "$ORIGINALITY", "Non-Original")
edits(meta, updates)
let row_offsets : Array[Int] = []
for c in cs {
for j = 0; j < self.channels[c].bits / 8; j = j + 1 {
row_offsets.push(self.offsets[c] + j)
}
}
let width = row_offsets.length()
let raw = Bytes::makei(es.length() * width, i => {
self.raw[es[i / width] * self.width + row_offsets[i % width]]
})
assemble(meta, raw, if drop_analysis { [] } else { self.analysis })
}
///|
/// Event-major values. Input is finite; Float32 overflow is rejected.
pub fn create(
names : Array[String],
values : Array[Double],
double_precision? : Bool = false,
order? : ByteOrder = LittleEndian,
metadata? : Array[(String, String)] = [],
) -> Bytes raise FcsError {
if names.is_empty() ||
names.length() > 1024 ||
values.length() > 8000000 ||
values.length() % names.length() != 0 {
raise Invalid("creation shape")
}
let meta = [
("$MODE", "L"),
("$DATATYPE", if double_precision { "D" } else { "F" }),
("$BYTEORD", if order == LittleEndian { "1,2,3,4" } else { "4,3,2,1" }),
("$PAR", names.length().to_string()),
("$TOT", (values.length() / names.length()).to_string()),
("$ORIGINALITY", "Non-Original"),
]
let seen : Map[String, Bool] = Map([])
for i = 0; i < names.length(); i = i + 1 {
let name = names[i]
if name.is_empty() || seen.contains(name) {
raise Invalid("creation requires unique nonempty channel names")
}
seen[name] = true
put(meta, "$P\{i+1}N", name)
put(meta, "$P\{i+1}B", if double_precision { "64" } else { "32" })
put(meta, "$P\{i+1}R", "262144")
put(meta, "$P\{i+1}E", "0,0")
}
edits(meta, metadata)
let out : Array[Byte] = []
for v in values {
if !finite(v) {
raise Invalid("creation requires finite values")
}
let bits = if double_precision {
v.reinterpret_as_uint64()
} else {
let f = Float::from_double(v)
if !finite(f.to_double()) {
raise Invalid("Float32 value overflow")
}
f.reinterpret_as_uint().to_uint64()
}
emit_word(out, bits, if double_precision { 8 } else { 4 }, order)
}
assemble(meta, Bytes::from_array(out), [])
}