Skip to content

Commit

Permalink
feat(ansi): func to convert byte pos to char pos (#340)
Browse files Browse the repository at this point in the history
* feat(ansi): func to convert byte pos to char pos

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* docs: typo

* fix: rename

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
  • Loading branch information
caarlos0 authored Jan 23, 2025
1 parent a969dde commit a5138d3
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 1 deletion.
2 changes: 1 addition & 1 deletion ansi/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/charmbracelet/x/ansi

go 1.18
go 1.21

require (
github.com/lucasb-eyer/go-colorful v1.2.0
Expand Down
25 changes: 25 additions & 0 deletions ansi/truncate.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,28 @@ func TruncateLeft(s string, n int, prefix string) string {

return buf.String()
}

// ByteToGraphemeRange takes start and stop byte positions and converts them to
// grapheme-aware char positions.
// You can use this with [Truncate], [TruncateLeft], and [Cut].
func ByteToGraphemeRange(str string, byteStart, byteStop int) (charStart, charStop int) {
bytePos, charPos := 0, 0
gr := uniseg.NewGraphemes(str)
for byteStart > bytePos {
if !gr.Next() {
break
}
bytePos += len(gr.Str())
charPos += max(1, gr.Width())
}
charStart = charPos
for byteStop > bytePos {
if !gr.Next() {
break
}
bytePos += len(gr.Str())
charPos += max(1, gr.Width())
}
charStop = charPos
return
}
46 changes: 46 additions & 0 deletions ansi/truncate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,49 @@ func TestCut(t *testing.T) {
})
}
}

func TestByteToGraphemeRange(t *testing.T) {
cases := []struct {
name string
feed [2]int
expect [2]int
input string
}{
{
name: "simple",
input: "hello world from x/ansi",
feed: [2]int{2, 9},
expect: [2]int{2, 9},
},
{
name: "with emoji",
input: " Downloads",
feed: [2]int{4, 7},
expect: [2]int{2, 5},
},
{
name: "start out of bounds",
input: "some text",
feed: [2]int{-1, 5},
expect: [2]int{0, 5},
},
{
name: "end out of bounds",
input: "some text",
feed: [2]int{1, 50},
expect: [2]int{1, 9},
},
}

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
charStart, charStop := ByteToGraphemeRange(tt.input, tt.feed[0], tt.feed[1])
if expect := tt.expect[0]; expect != charStart {
t.Errorf("expected start to be %d, got %d", expect, charStart)
}
if expect := tt.expect[1]; expect != charStop {
t.Errorf("expected stop to be %d, got %d", expect, charStop)
}
})
}
}

0 comments on commit a5138d3

Please sign in to comment.