///|
pub fn is_surrogate_code_unit(unit : UInt16) -> Bool {
unit >= 0xD800 && unit <= 0xDFFF
}
///|
pub fn string_has_non_bmp(text : String) -> Bool {
text.to_array().any(fn(c) { !c.is_bmp() })
}
///|
pub fn display_width(text : String) -> Int {
text
.to_array()
.fold(init=0, (width, c) => {
if is_cjk_char(c) || !c.is_bmp() {
width + 2
} else if c.is_control() {
width
} else {
width + 1
}
})
}
///|
pub fn code_unit_offsets(text : String) -> Array[Int] {
let result = [0]
let mut offset = 0
for c in text {
offset += c.to_string().length()
result.push(offset)
}
result
}
///|
pub fn char_index_to_code_unit(text : String, index : Int) -> Int {
let offsets = code_unit_offsets(text)
match offsets.get(index) {
Some(value) => value
None => offsets[offsets.length() - 1]
}
}
///|
pub fn code_unit_to_char_index(text : String, offset : Int) -> Int {
let offsets = code_unit_offsets(text)
let bounded = if offset < 0 {
0
} else if offset > text.length() {
text.length()
} else {
offset
}
let mut result = 0
for i in 0.. Span {
{
start: char_index_to_code_unit(text, span.start),
end: char_index_to_code_unit(text, span.end),
}
}
///|
pub fn code_unit_span_to_chars(text : String, span : Span) -> Span {
{
start: code_unit_to_char_index(text, span.start),
end: code_unit_to_char_index(text, span.end),
}
}
///|
pub fn normalize_newlines(text : String) -> String {
text.replace_all(old="\r\n", new="\n").replace_all(old="\r", new="\n")
}
///|
pub fn remove_zero_width_chars(text : String) -> String {
let builder = StringBuilder()
for c in text {
let value = c.to_int()
if value != 0x200B && value != 0x200C && value != 0x200D && value != 0xFEFF {
builder.write_char(c)
}
}
builder.to_string()
}
///|
pub fn sanitize_input(text : String) -> String {
normalize_newlines(remove_zero_width_chars(text))
}
///|
pub fn safe_normalized_input(text : String) -> String {
normalize_for_matching(sanitize_input(text))
}
///|
pub fn normalize_with_offset_notice(text : String) -> (String, Bool) {
let normalized = sanitize_input(text)
(normalized, normalized != text)
}
///|
pub fn replacement_boundary_is_safe(text : String, offset : Int) -> Bool {
if offset <= 0 || offset >= text.length() {
true
} else {
let left = text[offset - 1]
let right = text[offset]
!(left >= 0xD800 && left <= 0xDBFF && right >= 0xDC00 && right <= 0xDFFF)
}
}
///|
pub fn safe_code_unit_boundaries(text : String) -> Array[Int] {
let result = []
for offset in 0..<=text.length() {
if replacement_boundary_is_safe(text, offset) {
result.push(offset)
}
}
result
}
///|
pub fn nearest_safe_boundary(text : String, offset : Int) -> Int {
let bounded = if offset < 0 {
0
} else if offset > text.length() {
text.length()
} else {
offset
}
if replacement_boundary_is_safe(text, bounded) {
bounded
} else {
let mut left = bounded
while left > 0 && !replacement_boundary_is_safe(text, left) {
left -= 1
}
left
}
}
///|
pub fn safe_slice_by_code_units(
text : String,
start : Int,
end : Int,
) -> String {
let left = nearest_safe_boundary(text, start)
let right = nearest_safe_boundary(text, end)
if right < left {
""
} else {
text[left:right].to_owned()
}
}
///|
pub fn redact_unicode_safe(
input : String,
config : RedactionConfig,
) -> DeidResult raise DeidError {
let sanitized = sanitize_input(input)
redact_with_config(sanitized, config)
}
///|
pub fn unicode_profile(text : String) -> Map[String, Int] {
let counts : Map[String, Int] = Map([])
for c in text {
let kind = if c.is_ascii() {
"ascii"
} else if c.is_bmp() {
"bmp"
} else {
"non-bmp"
}
counts[kind] = counts.get_or_default(kind, 0) + 1
}
counts
}
///|
pub fn unicode_profile_text(text : String) -> String {
let profile = unicode_profile(text)
[
"characters=\{text.char_length()}",
"code_units=\{text.length()}",
"display_width=\{display_width(text)}",
"ascii=\{profile.get_or_default("ascii", 0)}",
"bmp=\{profile.get_or_default("bmp", 0)}",
"non_bmp=\{profile.get_or_default("non-bmp", 0)}",
].join("\n")
}