///|
pub enum Delimiter {
Comma
Tab
} derive(Debug, Eq, ToJson)
///|
pub fn Delimiter::comma() -> Delimiter {
Comma
}
///|
pub fn Delimiter::tab() -> Delimiter {
Tab
}
///|
pub struct ImportOptions {
has_header : Bool
delimiter : Delimiter
reject_unknown_columns : Bool
} derive(Debug, Eq, ToJson)
///|
pub fn ImportOptions::csv() -> ImportOptions {
{ has_header: true, delimiter: Comma, reject_unknown_columns: true }
}
///|
pub fn ImportOptions::tsv() -> ImportOptions {
{ has_header: true, delimiter: Tab, reject_unknown_columns: true }
}
///|
pub fn ImportOptions::new(
has_header : Bool,
delimiter : Delimiter,
reject_unknown_columns : Bool,
) -> ImportOptions {
{ has_header, delimiter, reject_unknown_columns }
}
///|
pub struct ImportDiagnostic {
line : Int
column : Int
message : String
hint : String
} derive(Debug, Eq, ToJson)
///|
pub struct ImportResult {
entries : Array[IcdEntry]
diagnostics : Array[ImportDiagnostic]
} derive(Debug, Eq, ToJson)
///|
pub fn import_entries(text : String, options : ImportOptions) -> ImportResult {
let diagnostics = []
let rows = scan_delimited(text, options.delimiter, diagnostics)
let header_offset = if options.has_header { 1 } else { 0 }
if options.has_header && rows.length() == 0 {
diagnostics.push(
import_diagnostic(
1, 1, "missing header", "provide the six required column names",
),
)
return { entries: [], diagnostics }
}
if options.has_header {
validate_header(rows[0], options, diagnostics)
}
let entries = []
for row_index in header_offset.. entries.push(entry)
None => ()
}
}
if diagnostics.is_empty() {
try Catalog::from_entries(entries) catch {
CatalogError::DuplicateCode(code) =>
diagnostics.push(
import_diagnostic(
0,
0,
"duplicate code: " + code,
"remove the duplicate row",
),
)
CatalogError::MissingReference(code, reference) =>
diagnostics.push(
import_diagnostic(
0,
0,
"missing reference from " + code + ": " + reference,
"include the referenced code or remove the relation",
),
)
CatalogError::InvalidEntry(code, message) =>
diagnostics.push(
import_diagnostic(
0,
0,
code + ": " + message,
"fix the row and import again",
),
)
} noraise {
_ => ()
}
}
let normalized_entries = if diagnostics.is_empty() {
Catalog::from_entries(entries).all() catch {
_ => []
}
} else {
[]
}
if diagnostics.is_empty() {
{ entries: normalized_entries, diagnostics }
} else {
{ entries: [], diagnostics }
}
}
///|
fn import_diagnostic(
line : Int,
column : Int,
message : String,
hint : String,
) -> ImportDiagnostic {
{ line, column, message, hint }
}
///|
fn validate_header(
row : Array[String],
options : ImportOptions,
diagnostics : Array[ImportDiagnostic],
) -> Unit {
let expected = ["code", "title", "chapter_id", "parent", "excludes", "note"]
if row.length() != expected.length() {
diagnostics.push(
import_diagnostic(
1,
row.length() + 1,
"invalid header width",
"use the six required column names",
),
)
return
}
for i in 0.. IcdEntry? {
let code = row[0].trim().to_owned()
if code.is_empty() {
diagnostics.push(
import_diagnostic(line, 1, "code is empty", "provide an ICD-10 code"),
)
return None
}
let parent = if row[3].trim().is_empty() {
None
} else {
Some(row[3].trim().to_owned())
}
let excludes = if row[4].trim().is_empty() {
[]
} else {
split_exclusions(row[4])
}
Some(
IcdEntry::new(
code,
row[1],
row[2].trim().to_owned(),
parent,
excludes,
row[5],
),
)
}
///|
fn split_exclusions(value : String) -> Array[String] {
let result = []
let mut current = ""
for c in value {
if c == ';' {
result.push(current.trim().to_owned())
current = ""
} else {
current = current + c.to_string()
}
}
if !current.trim().is_empty() {
result.push(current.trim().to_owned())
}
result
}
///|
fn delimiter_char(delimiter : Delimiter) -> Char {
match delimiter {
Comma => ','
Tab => '\t'
}
}
///|
fn scan_delimited(
text : String,
delimiter : Delimiter,
diagnostics : Array[ImportDiagnostic],
) -> Array[Array[String]] {
let rows = []
let row = []
let mut field = ""
let mut quoted = false
let mut line = 1
let separator = delimiter_char(delimiter)
let mut i = 0
while i < text.length() {
let current = text.get_char(i).unwrap()
if quoted {
if current == '"' {
if i + 1 < text.length() && text.get_char(i + 1).unwrap() == '"' {
field = field + "\""
i += 1
} else {
quoted = false
}
} else {
field = field + current.to_string()
if current == '\n' {
line += 1
}
}
} else if current == '"' && field.is_empty() {
quoted = true
} else if current == separator {
row.push(field)
field = ""
} else if current == '\n' {
row.push(field)
field = ""
rows.push(row.copy())
row.clear()
line += 1
} else if current != '\r' {
field = field + current.to_string()
}
i += 1
}
if quoted {
diagnostics.push(
import_diagnostic(
line,
field.length() + 1,
"unterminated quoted field",
"close the quoted value with a double quote",
),
)
}
if !field.is_empty() || !row.is_empty() {
row.push(field)
rows.push(row)
}
rows
}