///|
/// Dependency-free text and delimited-data utilities.
///
/// Model files and benchmark artifacts often need a tiny parser but should not
/// pull an IO or JSON dependency into the core package. These routines cover
/// stable tokenization, key-value configuration, CSV rows, and redaction.
pub struct TextToken {
value : String
line : Int
column : Int
}
///|
/// Create a token.
pub fn text_token(value : String, line : Int, column : Int) -> TextToken {
{ value, line, column }
}
///|
/// Read token value.
pub fn TextToken::value(self : TextToken) -> String {
self.value
}
///|
/// Read token line.
pub fn TextToken::line(self : TextToken) -> Int {
self.line
}
///|
/// Read token column.
pub fn TextToken::column(self : TextToken) -> Int {
self.column
}
///|
/// Tokenize non-whitespace runs while preserving locations.
pub fn tokenize_text(input : String) -> Array[TextToken] {
let result : Array[TextToken] = []
let mut token = ""
let mut line = 0
let mut column = 0
let mut token_line = 0
let mut token_column = 0
let flush = () => {
if token != "" {
result.push(text_token(token, token_line, token_column))
token = ""
}
}
for character in input {
if character == '\n' {
flush()
line += 1
column = 0
} else if character.is_whitespace() {
flush()
column += 1
} else {
if token == "" {
token_line = line
token_column = column
}
token += character.to_string()
column += 1
}
}
flush()
result
}
///|
/// A text document with line access.
pub struct TextDocument {
lines : Array[String]
}
///|
/// Split a document into owned lines.
pub fn text_document(input : String) -> TextDocument {
let lines : Array[String] = []
for line in input.split("\n") {
lines.push(line.to_owned())
}
{ lines, }
}
///|
/// Return line count.
pub fn TextDocument::line_count(self : TextDocument) -> Int {
self.lines.length()
}
///|
/// Return one line.
pub fn TextDocument::line(self : TextDocument, index : Int) -> String {
if index < 0 || index >= self.lines.length() {
""
} else {
self.lines[index]
}
}
///|
/// Return copied lines.
pub fn TextDocument::lines(self : TextDocument) -> Array[String] {
self.lines.copy()
}
///|
/// Return non-empty line numbers.
pub fn TextDocument::nonempty_lines(self : TextDocument) -> Array[Int] {
let result : Array[Int] = []
for index, line in self.lines {
if line.trim() != "" {
result.push(index)
}
}
result
}
///|
/// Return comment-free lines with a configured prefix.
pub fn TextDocument::without_comments(
self : TextDocument,
prefix : String,
) -> Array[String] {
let result : Array[String] = []
for line in self.lines {
if !line.trim().has_prefix(prefix) {
result.push(line)
}
}
result
}
///|
/// Parse key-value pairs separated by `=`.
pub fn parse_key_values(input : String) -> Array[(String, String)] {
let result : Array[(String, String)] = []
for line_view in input.split("\n") {
let raw = line_view.to_owned()
let line = raw.trim().to_owned()
if line == "" || line.has_prefix("#") {
continue
}
match text_find_char(line, '=') {
Some(index) => {
let key = line[0:index].to_owned().trim().to_owned()
let value = line[index + 1:].to_owned().trim().to_owned()
result.push((key, value))
}
None => ()
}
}
result
}
///|
/// Find a key in key-value pairs.
pub fn key_value(pairs : Array[(String, String)], key : String) -> String? {
for pair in pairs {
if pair.0 == key {
return Some(pair.1)
}
}
None
}
///|
/// Parse one CSV row with quoted fields.
pub fn parse_csv_row(input : String) -> Array[String] {
let result : Array[String] = []
let mut field = ""
let mut quoted = false
for character in input {
if character == '"' {
quoted = !quoted
} else if character == ',' && !quoted {
result.push(field)
field = ""
} else {
field += character.to_string()
}
}
result.push(field)
result
}
///|
/// Parse a CSV document.
pub fn parse_csv(input : String) -> Array[Array[String]] {
let result : Array[Array[String]] = []
for line in input.split("\n") {
let owned = line.to_owned()
if owned.trim() != "" {
result.push(parse_csv_row(owned))
}
}
result
}
///|
/// Render a CSV row with quote escaping.
pub fn render_csv_row(fields : Array[String]) -> String {
let builder = StringBuilder()
for index, field in fields {
if index > 0 {
builder.write_char(',')
}
let needs_quotes = field.contains(",") ||
field.contains("\"") ||
field.contains("\n")
if needs_quotes {
builder.write_char('"')
}
for character in field {
if character == '"' {
builder.write_string("\"\"")
} else {
builder.write_char(character)
}
}
if needs_quotes {
builder.write_char('"')
}
}
builder.to_string()
}
///|
/// Render CSV rows.
pub fn render_csv(rows : Array[Array[String]]) -> String {
let builder = StringBuilder()
for index, row in rows {
if index > 0 {
builder.write_char('\n')
}
builder.write_string(render_csv_row(row))
}
builder.to_string()
}
///|
/// Return all integer tokens in a document.
pub fn integer_tokens(input : String) -> Array[Int] {
let result : Array[Int] = []
for token in tokenize_text(input) {
match parse_integer_list(token.value) {
Some(values) => if values.length() == 1 { result.push(values[0]) }
None => ()
}
}
result
}
///|
/// Replace all occurrences of a substring.
pub fn replace_text(
input : String,
target : String,
replacement : String,
) -> String {
if target == "" {
return input
}
let builder = StringBuilder()
let mut start = 0
while start < input.length() {
match text_find_substring(input, target, start) {
Some(index) => {
builder.write_string(input[start:index].to_owned())
builder.write_string(replacement)
start = index + target.length()
}
None => {
builder.write_string(input[start:].to_owned())
start = input.length()
}
}
}
builder.to_string()
}
///|
/// Find a character in an owned string.
fn text_find_char(input : String, target : Char) -> Int? {
for index in 0.. if character == target { return Some(index) }
None => return None
}
}
None
}
///|
/// Find a substring at or after an offset.
fn text_find_substring(input : String, target : String, offset : Int) -> Int? {
if target == "" {
return Some(offset)
}
for index in offset.. String {
let result : Array[String] = []
for pair in parse_key_values(input) {
let mut value = pair.1
if keys.contains(pair.0) {
value = replacement
}
result.push(pair.0 + "=" + value)
}
let builder = StringBuilder()
for index, line in result {
if index > 0 {
builder.write_char('\n')
}
builder.write_string(line)
}
builder.to_string()
}
///|
/// Return a stable text fingerprint.
pub fn text_signature(input : String) -> Int {
let mut result = 17
for character in input {
result = result * 31 + character.to_int()
}
result
}
///|
/// Return whether all lines have a maximum width.
pub fn lines_within_width(input : String, width : Int) -> Bool {
for line in input.split("\n") {
if line.length() > width {
return false
}
}
true
}
///|
/// Return the longest line length.
pub fn longest_line(input : String) -> Int {
let mut result = 0
for line in input.split("\n") {
if line.length() > result {
result = line.length()
}
}
result
}
///|
/// Return word frequencies in first-seen order.
pub fn word_frequency(input : String) -> Array[(String, Int)] {
let result : Array[(String, Int)] = []
for token in tokenize_text(input) {
let mut found = false
for index, pair in result {
if pair.0 == token.value {
result[index] = (pair.0, pair.1 + 1)
found = true
}
}
if !found {
result.push((token.value, 1))
}
}
result
}