///|
fn value_bytes(
value : Value,
field : Field,
encoding : Encoding,
offset : Int,
) -> Bytes raise DbfError {
let data = match (field.kind, value) {
(_, Missing) => b""
(Character, Text(s)) => encode(s, encoding, offset)
(Numeric, Number(s)) | (Float, Number(s)) => {
valid_number(s, field, offset)
@utf8.encode(s[:])
}
(Date, DateValue(s)) => {
valid_date(s, offset)
@utf8.encode(s[:])
}
(Logical, LogicalValue(v)) => if v { b"T" } else { b"F" }
_ => raise Invalid(offset, "value type does not match field")
}
if data.length() > field.width {
raise Invalid(offset, "encoded value exceeds field width")
}
data
}
///|
pub fn validate_table(table : Table) -> Unit raise DbfError {
ignore(validate_fields(table.fields))
for i, row in table.rows {
if row.values.length() != table.fields.length() {
raise Invalid(i, "row has wrong number of values")
}
for j, value in row.values {
ignore(value_bytes(value, table.fields[j], Utf8, i))
}
}
}
///|
pub fn write(
table : Table,
encoding? : Encoding = Utf8,
update_date? : UpdateDate = { year: 2000, month: 1, day: 1 },
limits? : Limits = default_limits(),
) -> Bytes raise DbfError {
check_limits(limits)
check_update_date(update_date)
let width = validate_fields(table.fields)
let header = 33 + 32 * table.fields.length()
if table.rows.length() > (2147483646 - header) / width {
raise Invalid(4, "table exceeds supported byte length")
}
if table.rows.length() > limits.max_records ||
table.fields.length() > limits.max_fields ||
header + table.rows.length() * width + 1 > limits.max_bytes {
raise Invalid(0, "output exceeds resource limits")
}
// Validate every value before allocating the output buffer.
for i, row in table.rows {
if row.values.length() != table.fields.length() {
raise Invalid(i, "row has wrong number of values")
}
for j, value in row.values {
ignore(value_bytes(value, table.fields[j], encoding, header + i * width))
}
}
let data = Array::make(header + table.rows.length() * width + 1, b'\x00')
data[0] = b'\x03'
// Deterministic valid modification date: 2000-01-01.
data[1] = (update_date.year - 1900).to_byte()
data[2] = update_date.month.to_byte()
data[3] = update_date.day.to_byte()
for i = 0; i < 4; i = i + 1 {
data[4 + i] = (table.rows.length() >> (i * 8)).to_byte()
}
data[8] = header.to_byte()
data[9] = (header >> 8).to_byte()
data[10] = width.to_byte()
data[11] = (width >> 8).to_byte()
for i, field in table.fields {
let p = 32 + i * 32
let name = @utf8.encode(field.name[:])
for j, b in name {
data[p + j] = b
}
data[p + 11] = match field.kind {
Character => b'C'
Numeric => b'N'
Float => b'F'
Date => b'D'
Logical => b'L'
}
data[p + 16] = field.width.to_byte()
data[p + 17] = field.decimals.to_byte()
}
data[header - 1] = b'\x0d'
for i, row in table.rows {
let mut p = header + i * width
data[p] = if row.deleted { b'*' } else { b' ' }
p += 1
for j, value in row.values {
let f = table.fields[j]
let bytes = value_bytes(value, f, encoding, p)
for k = 0; k < f.width; k = k + 1 {
data[p + k] = b' '
}
let skip = match f.kind {
Numeric | Float => f.width - bytes.length()
_ => 0
}
for k, b in bytes {
data[p + skip + k] = b
}
p += f.width
}
}
data[data.length() - 1] = b'\x1a'
Bytes::from_array(data)
}
///|
/// Exact, case-sensitive field lookup. Missing fields are explicit.
pub fn field_index(table : Table, name : String) -> Int? {
for i, f in table.fields {
if f.name == name {
return Some(i)
}
}
None
}
///|
/// Reorders or selects columns while retaining every physical row and deletion flag.
pub fn project(table : Table, names : Array[String]) -> Table raise DbfError {
let fields : Array[Field] = []
let indices : Array[Int] = []
for name in names {
match field_index(table, name) {
Some(i) => {
fields.push(table.fields[i])
indices.push(i)
}
None => raise Invalid(0, "unknown projection field: " + name)
}
}
ignore(validate_fields(fields))
let rows : Array[Row] = []
for i, row in table.rows {
if row.values.length() != table.fields.length() {
raise Invalid(i, "row has wrong number of values")
}
rows.push({
deleted: row.deleted,
values: indices.map(fn(j) { row.values[j] }),
})
}
{ fields, rows }
}