///|
/// Build a string from a character array without relying on a particular
/// backend's string representation.
pub fn chars_to_string(chars : Array[Char]) -> String {
let builder = StringBuilder()
for c in chars {
builder.write_char(c)
}
builder.to_string()
}
///|
pub fn char_count(text : String) -> Int {
text.char_length()
}
///|
pub fn code_unit_count(text : String) -> Int {
text.length()
}
///|
pub fn is_ascii_char(c : Char) -> Bool {
c.is_ascii()
}
///|
pub fn is_ascii_digit_char(c : Char) -> Bool {
c.is_ascii_digit()
}
///|
pub fn is_ascii_letter_char(c : Char) -> Bool {
c.is_ascii_alphabetic()
}
///|
pub fn is_ascii_alphanumeric_char(c : Char) -> Bool {
c.is_ascii_digit() || c.is_ascii_alphabetic()
}
///|
pub fn is_hex_char(c : Char) -> Bool {
c.is_ascii_hexdigit()
}
///|
pub fn is_word_char(c : Char) -> Bool {
is_ascii_alphanumeric_char(c) || c == '_' || c.is_numeric()
}
///|
pub fn is_cjk_char(c : Char) -> Bool {
let value = c.to_int()
(value >= 0x3400 && value <= 0x4DBF) ||
(value >= 0x4E00 && value <= 0x9FFF) ||
(value >= 0xF900 && value <= 0xFAFF)
}
///|
pub fn is_blank_char(c : Char) -> Bool {
c.is_whitespace() || c == '\u{3000}'
}
///|
pub fn is_ascii_punctuation_char(c : Char) -> Bool {
c.is_ascii_punctuation()
}
///|
pub fn all_ascii_digits(text : String) -> Bool {
if text.is_empty() {
false
} else {
text.to_array().all(is_ascii_digit_char)
}
}
///|
pub fn all_ascii_hex(text : String) -> Bool {
if text.is_empty() {
false
} else {
text.to_array().all(is_hex_char)
}
}
///|
pub fn contains_cjk(text : String) -> Bool {
text.to_array().any(is_cjk_char)
}
///|
pub fn contains_ascii_letter(text : String) -> Bool {
text.to_array().any(is_ascii_letter_char)
}
///|
pub fn trim_ascii_space(text : String) -> String {
text.trim(chars=" \t\r\n").to_owned()
}
///|
pub fn collapse_spaces(text : String) -> String {
let builder = StringBuilder()
let mut pending_space = false
for c in text {
if is_blank_char(c) {
pending_space = true
} else {
if pending_space && !builder.is_empty() {
builder.write_char(' ')
}
builder.write_char(c)
pending_space = false
}
}
builder.to_string()
}
///|
pub fn normalize_punctuation(text : String) -> String {
let builder = StringBuilder()
for c in text {
match c {
':' => builder.write_char(':')
',' => builder.write_char(',')
';' => builder.write_char(';')
'(' => builder.write_char('(')
')' => builder.write_char(')')
'[' => builder.write_char('[')
']' => builder.write_char(']')
'-' | '—' | '–' => builder.write_char('-')
'/' => builder.write_char('/')
'.' => builder.write_char('.')
'@' => builder.write_char('@')
_ => builder.write_char(c)
}
}
builder.to_string()
}
///|
pub fn normalize_for_matching(text : String) -> String {
collapse_spaces(normalize_punctuation(text))
}
///|
pub fn keep_digits(text : String) -> String {
let builder = StringBuilder()
for c in text {
if c.is_ascii_digit() {
builder.write_char(c)
}
}
builder.to_string()
}
///|
pub fn keep_ascii_letters_and_digits(text : String) -> String {
let builder = StringBuilder()
for c in text {
if is_ascii_alphanumeric_char(c) {
builder.write_char(c)
}
}
builder.to_string()
}
///|
pub fn ascii_lower(text : String) -> String {
text.to_lower()
}
///|
pub fn ascii_upper(text : String) -> String {
text.to_upper()
}
///|
pub fn repeated_char(c : Char, count : Int) -> String {
if count <= 0 {
""
} else {
String::make(count, c)
}
}
///|
pub fn safe_char_slice(text : String, start : Int, end : Int) -> String {
let chars = text.to_array()
let lower = if start < 0 {
0
} else if start > chars.length() {
chars.length()
} else {
start
}
let upper = if end < lower {
lower
} else if end > chars.length() {
chars.length()
} else {
end
}
let builder = StringBuilder()
for i in lower.. Char? {
text.to_array().get(0)
}
///|
pub fn last_char(text : String) -> Char? {
let chars = text.to_array()
if chars.is_empty() {
None
} else {
chars.get(chars.length() - 1)
}
}
///|
pub fn char_at_or(text : String, position : Int, fallback : Char) -> Char {
match text.to_array().get(position) {
Some(c) => c
None => fallback
}
}
///|
pub fn starts_with_word(text : String, prefix : String) -> Bool {
if !text.has_prefix(prefix) {
false
} else if text.length() == prefix.length() {
true
} else {
match text.get_char(prefix.length()) {
Some(c) => !is_word_char(c)
None => true
}
}
}
///|
pub fn ends_with_word(text : String, suffix : String) -> Bool {
if !text.has_suffix(suffix) {
false
} else if text.length() == suffix.length() {
true
} else {
match text.get_char(text.length() - suffix.length() - 1) {
Some(c) => !is_word_char(c)
None => true
}
}
}
///|
pub fn boundary_before(text : String, position : Int) -> Bool {
if position <= 0 {
true
} else {
match text.get_char(position - 1) {
Some(c) => !is_word_char(c)
None => true
}
}
}
///|
pub fn boundary_after(text : String, position : Int) -> Bool {
if position >= text.length() {
true
} else {
match text.get_char(position) {
Some(c) => !is_word_char(c)
None => true
}
}
}
///|
pub fn is_word_boundary(text : String, start : Int, end : Int) -> Bool {
boundary_before(text, start) && boundary_after(text, end)
}
///|
pub fn line_count(text : String) -> Int {
if text.is_empty() {
0
} else {
let mut count = 1
for c in text {
if c == '\n' {
count += 1
}
}
count
}
}
///|
pub fn line_column(text : String, offset : Int) -> LineColumn {
let bounded = if offset < 0 {
0
} else if offset > text.length() {
text.length()
} else {
offset
}
let mut line = 1
let mut column = 1
for i in 0.. TextWindow {
let safe_radius = if radius < 0 { 0 } else { radius }
let span = Span::{ start: center - safe_radius, end: center + safe_radius }.clamp(
text.length(),
)
{
start: span.start,
end: span.end,
text: text[span.start:span.end].to_owned(),
}
}
///|
pub fn split_lines_with_offsets(text : String) -> Array[TextWindow] {
let windows = []
let mut start = 0
for i in 0.. String {
let missing = width - text.char_length()
if missing <= 0 {
text
} else {
repeated_char(fill, missing) + text
}
}
///|
pub fn pad_right(text : String, width : Int, fill : Char) -> String {
let missing = width - text.char_length()
if missing <= 0 {
text
} else {
text + repeated_char(fill, missing)
}
}
///|
pub fn quote_for_log(text : String) -> String {
let builder = StringBuilder()
builder.write_char('"')
for c in text {
match c {
'"' => builder.write_string("\\\"")
'\\' => builder.write_string("\\\\")
'\n' => builder.write_string("\\n")
'\r' => builder.write_string("\\r")
'\t' => builder.write_string("\\t")
_ => builder.write_char(c)
}
}
builder.write_char('"')
builder.to_string()
}
///|
pub fn numeric_suffix(text : String) -> String {
let chars = text.to_array()
let mut start = chars.length()
while start > 0 && chars[start - 1].is_ascii_digit() {
start -= 1
}
let builder = StringBuilder()
for i in start.. String {
let chars = text.to_array()
let mut end = 0
while end < chars.length() && chars[end].is_ascii_digit() {
end += 1
}
let builder = StringBuilder()
for i in 0.. Map[String, Int] {
let counts : Map[String, Int] = Map([])
for c in text {
let key = c.to_string()
counts[key] = counts.get_or_default(key, 0) + 1
}
counts
}
///|
pub fn count_char(text : String, target : Char) -> Int {
let mut result = 0
for c in text {
if c == target {
result += 1
}
}
result
}
///|
pub fn count_substring(text : String, needle : String) -> Int {
if needle.is_empty() {
0
} else {
let mut count = 0
let mut cursor = 0
while cursor + needle.length() <= text.length() {
if text[cursor:cursor + needle.length()] == needle {
count += 1
cursor += needle.length()
} else {
cursor += 1
}
}
count
}
}
///|
pub fn longest_common_prefix(left : String, right : String) -> String {
let limit = if left.length() < right.length() {
left.length()
} else {
right.length()
}
let mut end = 0
while end < limit && left[end] == right[end] {
end += 1
}
left[:end].to_owned()
}
///|
pub fn redactable_char_count(text : String) -> Int {
text.to_array().filter(fn(c) { !is_blank_char(c) }).length()
}