///|
fn first_field_separator(line : String) -> (Int, String)? {
for i in 0.. FieldValue? {
let trimmed_start = line.length() - line.trim_start().length()
let trimmed = line.trim().to_owned()
match first_field_separator(trimmed) {
Some((separator, token)) => {
let key = trimmed[:separator].trim().to_owned()
let value_start = separator + token.length()
let value = trimmed[value_start:].trim().to_owned()
if key.is_empty() {
None
} else {
Some({
key,
value,
start: offset + trimmed_start + value_start,
end: offset + trimmed_start + trimmed.length(),
separator: token,
})
}
}
None => None
}
}
///|
pub fn parse_fields(text : String) -> Array[FieldValue] {
split_lines_with_offsets(text).fold(init=[], (result, line) => {
match parse_field_line(line.text, line.start) {
Some(field) => {
result.push(field)
result
}
None => result
}
})
}
///|
pub fn fields_named(
fields : Array[FieldValue],
key : String,
) -> Array[FieldValue] {
fields.filter(fn(item) { item.key.to_lower() == key.to_lower() })
}
///|
pub fn first_field(fields : Array[FieldValue], key : String) -> FieldValue? {
fields_named(fields, key).get(0)
}
///|
pub fn field_values(fields : Array[FieldValue], key : String) -> Array[String] {
fields_named(fields, key).map(fn(item) { item.value })
}
///|
pub fn field_keys(fields : Array[FieldValue]) -> Array[String] {
fields.map(fn(item) { item.key })
}
///|
pub fn field_map(fields : Array[FieldValue]) -> Map[String, String] {
let result : Map[String, String] = Map([])
for item in fields {
result[item.key] = item.value
}
result
}
///|
pub fn redact_fields(
text : String,
config : RedactionConfig,
) -> String raise DeidError {
let fields = parse_fields(text)
let findings = []
for field in fields {
let result = redact_with_config(field.value, config)
for item in result.findings {
findings.push({
..item,
start: field.start + item.start,
end: field.start + item.end,
id: "field-\{field.key}-\{item.id}",
})
}
}
let adjusted = sort_findings(findings)
let (output, _) = apply_findings(text, adjusted)
output
}
///|
pub fn redact_field_values(
fields : Array[FieldValue],
config : RedactionConfig,
) -> Array[FieldValue] raise DeidError {
fields.map(field => {
let result = redact_with_config(field.value, config)
{ ..field, value: result.text, end: field.start + result.text.length() }
})
}
///|
pub fn render_fields(fields : Array[FieldValue], newline : String) -> String {
fields
.map(fn(field) { field.key + field.separator + field.value })
.join(newline)
}
///|
pub fn field_report(text : String) -> String {
let fields = parse_fields(text)
let lines = ["fields=\{fields.length()}"]
for field in fields {
lines.push(
"\{field.key}: \{field.start}..\{field.end} (\{field.value.length()})",
)
}
lines.join("\n")
}
///|
pub fn redact_named_fields(
text : String,
names : Array[String],
config : RedactionConfig,
) -> String raise DeidError {
let fields = parse_fields(text)
let selected = fields.filter(fn(item) { names.contains(item.key) })
let findings = []
for field in selected {
let result = redact_with_config(field.value, config)
for item in result.findings {
findings.push({
..item,
start: field.start + item.start,
end: field.start + item.end,
})
}
}
let (output, _) = apply_findings(text, sort_findings(findings))
output
}
///|
pub fn parse_delimited_row(
text : String,
delimiter : Char,
) -> Array[FieldValue] {
let result = []
let mut start = 0
let mut index = 0
for c in text {
if c == delimiter {
result.push({
key: "column-\{result.length()}",
value: text[start:index].to_owned(),
start,
end: index,
separator: delimiter.to_string(),
})
start = index + 1
}
index += c.to_string().length()
}
result.push({
key: "column-\{result.length()}",
value: text[start:].to_owned(),
start,
end: text.length(),
separator: delimiter.to_string(),
})
result
}
///|
pub fn render_delimited_row(
fields : Array[FieldValue],
delimiter : String,
) -> String {
fields.map(fn(item) { item.value }).join(delimiter)
}
///|
pub fn redact_delimited_row(
text : String,
delimiter : Char,
config : RedactionConfig,
) -> String raise DeidError {
let fields = parse_delimited_row(text, delimiter)
let redacted = redact_field_values(fields, config)
render_delimited_row(redacted, delimiter.to_string())
}
///|
pub fn field_values_length(fields : Array[FieldValue]) -> Int {
fields.fold(init=0, (sum, item) => sum + item.value.length())
}
///|
pub fn field_name_counts(fields : Array[FieldValue]) -> Map[String, Int] {
let result : Map[String, Int] = Map([])
for field in fields {
result[field.key] = result.get_or_default(field.key, 0) + 1
}
result
}
///|
pub fn fields_containing_phi(
fields : Array[FieldValue],
config : RedactionConfig,
) -> Array[String] raise DeidError {
let result = []
for field in fields {
if !redact_with_config(field.value, config).findings.is_empty() {
result.push(field.key)
}
}
result
}