Find the position of grapheme cluster breaks in a string
moon add hackwaly/find_cluster_break{
"import": ["hackwaly/find_cluster_break"]
}///|
test "basic usage" {
// Find the next cluster break after position 0
let result = @find_cluster_break.find_cluster_break("๐ช๐ฝ๐ฆ", 0)
inspect(result, content="4") // Points to start of butterfly emoji
}///|
test "find all cluster breaks" {
let text = "๐จโ๐ค๐ช๐ฝ๐ฉโ๐ฉโ๐งโ๐ฆ"
let breaks : Array[Int] = []
let mut pos = 0
while pos < text.length() {
let next_break = @find_cluster_break.find_cluster_break(text, pos)
if next_break == text.length() {
break
}
breaks.push(next_break)
pos = next_break
}
inspect(breaks, content="[5, 9]") // Positions of cluster boundaries
}///|
test "directional search" {
let text = "a๐จโ๐คb"
// Forward search (default)
let forward = @find_cluster_break.find_cluster_break(text, 0, forward=true)
inspect(forward, content="1")
// Backward search
let backward = @find_cluster_break.find_cluster_break(
text,
text.length(),
forward=false,
)
inspect(backward, content="6") // Start of last cluster
}///|
test "extending characters" {
let text = "รฉฬ " // 'e' + combining grave + combining left angle below
// With extending characters (default)
let with_extending = @find_cluster_break.find_cluster_break(
text,
0,
include_extending=true,
)
inspect(with_extending, content="2") // Treats the whole sequence as one cluster
// Without extending characters
let without_extending = @find_cluster_break.find_cluster_break(
text,
0,
include_extending=false,
)
inspect(without_extending, content="1") // Only the base 'e'
}///|
test "extending character detection" {
// Combining grave accent (U+0300)
let is_combining = @find_cluster_break.is_extending_char(768)
inspect(is_combining, content="true")
// Regular letter 'A'
let is_letter = @find_cluster_break.is_extending_char(65)
inspect(is_letter, content="false")
// Skin tone modifier (U+1F3FD)
let is_modifier = @find_cluster_break.is_extending_char(0x1F3FD)
inspect(is_modifier, content="true")
}///|
test "emoji sequences" {
// Man singer: man + ZWJ + microphone
let singer = "๐จโ๐ค"
let break_pos = @find_cluster_break.find_cluster_break(singer, 0)
inspect(break_pos, content="5") // Entire sequence is one cluster
// Flexed bicep with skin tone
let flexed = "๐ช๐ฝ"
let muscle_break = @find_cluster_break.find_cluster_break(flexed, 0)
inspect(muscle_break, content="4") // Emoji + skin tone modifier
}///|
test "flag emoji" {
let flags = "๐ฉ๐ช๐ซ๐ท๐ช๐ธ" // German, French, Spanish flags
let breaks : Array[Int] = []
let mut pos = 0
while pos < flags.length() {
let next = @find_cluster_break.find_cluster_break(flags, pos)
if next == flags.length() {
break
}
breaks.push(next)
pos = next
}
inspect(breaks, content="[4, 8]") // Each flag is 4 bytes (2 regional indicators)
}///|
test "accented characters" {
// Decomposed: 'e' + combining acute accent
let decomposed = "รฉ"
let d_break = @find_cluster_break.find_cluster_break(decomposed, 0)
inspect(d_break, content="1") // Treats as single cluster
// Complex accents: 'o' + multiple combining marks
let complex = "ลฬ " // o + double acute + left angle below
let c_break = @find_cluster_break.find_cluster_break(complex, 0)
inspect(c_break, content="2") // All combining marks included
}///|
test "visual character counting" {
fn count_grapheme_clusters(text : String) -> Int {
let mut count = 0
let mut pos = 0
while pos < text.length() {
let next = @find_cluster_break.find_cluster_break(text, pos)
if next == pos {
break
}
count = count + 1
pos = next
}
count
}
let text = "Hello ๐จโ๐ฉโ๐งโ๐ฆ World!"
let cluster_count = count_grapheme_clusters(text)
inspect(cluster_count, content="14") // Visual characters, not code units
// Compare with string length (code units)
let code_unit_length = text.length()
inspect(code_unit_length, content="24") // Much larger due to emoji
}///|
test "cluster-aware substring" {
fn substring_by_clusters(text : String, start : Int, length : Int) -> String {
let mut current_cluster = 0
let mut pos = 0
let mut start_pos = 0
let mut end_pos = text.length()
// Find start position
while current_cluster < start && pos < text.length() {
let next = @find_cluster_break.find_cluster_break(text, pos)
if next == pos {
break
}
current_cluster = current_cluster + 1
pos = next
if current_cluster == start {
start_pos = pos
}
}
// Find end position
let mut remaining = length
while remaining > 0 && pos < text.length() {
let next = @find_cluster_break.find_cluster_break(text, pos)
if next == pos {
break
}
remaining = remaining - 1
pos = next
if remaining == 0 {
end_pos = pos
}
}
text.substring(start=start_pos, end=end_pos)
}
let text = "๐โจ๐ฏ"
let sub = substring_by_clusters(text, 1, 1) // Get second cluster
inspect(sub, content="โจ") // Just the sparkles emoji
}fn find_cluster_break(str : String, pos : Int, forward? : Bool, include_extending? : Bool) -> IntFind the position of grapheme cluster breaks in a string