Skip to content

Commit 3f012b2

Browse files
add rtrunc, ltrunc, ctrunc for truncating strings
Co-Authored-By: Timothy <[email protected]>
1 parent f2f188d commit 3f012b2

File tree

5 files changed

+157
-0
lines changed

5 files changed

+157
-0
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ New library features
102102
the uniquing checking ([#53474])
103103
* `RegexMatch` objects can now be used to construct `NamedTuple`s and `Dict`s ([#50988])
104104
* `Lockable` is now exported ([#54595])
105+
* New `ltrunc`, `rtrunc` and `ctrunc` functions for truncating strings to text width, accounting for char widths ([#55351])
105106

106107
Standard library changes
107108
------------------------

base/exports.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,7 @@ export
596596
codepoint,
597597
codeunit,
598598
codeunits,
599+
ctrunc,
599600
digits,
600601
digits!,
601602
eachsplit,
@@ -620,6 +621,7 @@ export
620621
join,
621622
lpad,
622623
lstrip,
624+
ltrunc,
623625
ncodeunits,
624626
ndigits,
625627
nextind,
@@ -632,6 +634,7 @@ export
632634
rpad,
633635
rsplit,
634636
rstrip,
637+
rtrunc,
635638
split,
636639
string,
637640
strip,

base/strings/util.jl

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,126 @@ function rpad(
513513
r == 0 ? stringfn(s, p^q) : stringfn(s, p^q, first(p, r))
514514
end
515515

516+
"""
517+
rtrunc(str::AbstractString, w::Int, replace_str::AbstractString = "…")
518+
519+
Truncate `str` to at most `w` characters (see [`textwidth`](@ref)), replacing the last characters
520+
with `replace_str` if necessary. The default replacement string is "…".
521+
522+
# Examples
523+
```jldoctest
524+
julia> s = rtrunc("🍕🍕 I love 🍕", 10)
525+
"🍕🍕 I lo…"
526+
527+
julia> textwidth(s)
528+
10
529+
530+
julia> rtrunc("foo", 3)
531+
"foo"
532+
```
533+
534+
!!! compat "Julia 1.12"
535+
This function was added in Julia 1.12.
536+
537+
See also [`ltrunc`](@ref) and [`ctrunc`](@ref).
538+
"""
539+
function rtrunc(str::AbstractString, w::Int, replace_str::AbstractString = "")
540+
if textwidth(str) <= w
541+
return str
542+
else
543+
i, accwidth = firstindex(str), textwidth(replace_str)
544+
while true
545+
tw = textwidth(str[i])
546+
accwidth + tw <= w || break # no need to check if we go out of bounds because of the first if branch
547+
accwidth += tw
548+
i = nextind(str, i)
549+
end
550+
return str[firstindex(str):prevind(str, i)] * replace_str
551+
end
552+
end
553+
554+
"""
555+
ltrunc(str::AbstractString, w::Int, replace_str::AbstractString = "…")
556+
557+
Truncate `str` to at most `w` characters (see [`textwidth`](@ref)), replacing the first characters
558+
with `replace_str` if necessary. The default replacement string is "…".
559+
560+
# Examples
561+
```jldoctest
562+
julia> s = ltrunc("🍕🍕 I love 🍕", 10)
563+
"…I love 🍕"
564+
565+
julia> textwidth(s)
566+
10
567+
568+
julia> ltrunc("foo", 3)
569+
"foo"
570+
```
571+
572+
!!! compat "Julia 1.12"
573+
This function was added in Julia 1.12.
574+
575+
See also [`rtrunc`](@ref) and [`ctrunc`](@ref).
576+
"""
577+
function ltrunc(str::AbstractString, w::Int, replace_str::AbstractString = "")
578+
if textwidth(str) <= w
579+
return str
580+
else
581+
i, accwidth = lastindex(str), textwidth(replace_str)
582+
while true
583+
tw = textwidth(str[i])
584+
accwidth + tw <= w || break # no need to check if we go out of bounds because of the first if branch
585+
accwidth += tw
586+
i = prevind(str, i)
587+
end
588+
return replace_str * str[nextind(str, i):lastindex(str)]
589+
end
590+
end
591+
592+
"""
593+
ctrunc(str::AbstractString, w::Int, replace_str::AbstractString = "…"; prefer_left::Bool = true)
594+
595+
Truncate `str` to at most `w` characters (see [`textwidth`](@ref)), replacing the middle characters
596+
with `replace_str` if necessary. The default replacement string is "…". By default, the truncation
597+
prefers keeping chars on the left, but this can be changed by setting `prefer_left` to `false`.
598+
599+
# Examples
600+
```jldoctest
601+
julia> s = ctrunc("🍕🍕 I love 🍕", 10)
602+
"🍕🍕 …e 🍕"
603+
604+
julia> textwidth(s)
605+
10
606+
607+
julia> ctrunc("foo", 3)
608+
"foo"
609+
```
610+
611+
!!! compat "Julia 1.12"
612+
This function was added in Julia 1.12.
613+
614+
See also [`ltrunc`](@ref) and [`rtrunc`](@ref).
615+
"""
616+
function ctrunc(str::AbstractString, w::Int, replace_str::AbstractString = ""; prefer_left::Bool = true)
617+
if textwidth(str) <= w
618+
return str
619+
else
620+
l, r, accwidth = firstindex(str), lastindex(str), textwidth(replace_str)
621+
isleft = prefer_left
622+
while true
623+
tw = if isleft textwidth(str[l]) else textwidth(str[r]) end
624+
(accwidth += tw) <= w || break
625+
if isleft
626+
l = nextind(str, l)
627+
else
628+
r = prevind(str, r)
629+
end
630+
isleft = !isleft
631+
end
632+
return str[firstindex(str):prevind(str, l)] * replace_str * str[nextind(str, r):lastindex(str)]
633+
end
634+
end
635+
516636
"""
517637
eachsplit(str::AbstractString, dlm; limit::Integer=0, keepempty::Bool=true)
518638
eachsplit(str::AbstractString; limit::Integer=0, keepempty::Bool=false)

doc/src/base/strings.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ Base.:(==)(::AbstractString, ::AbstractString)
4848
Base.cmp(::AbstractString, ::AbstractString)
4949
Base.lpad
5050
Base.rpad
51+
Base.ltrunc
52+
Base.rtrunc
53+
Base.ctrunc
5154
Base.findfirst(::AbstractString, ::AbstractString)
5255
Base.findnext(::AbstractString, ::AbstractString, ::Integer)
5356
Base.findnext(::AbstractChar, ::AbstractString, ::Integer)

test/strings/util.jl

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,36 @@ SubStr(s) = SubString("abc$(s)de", firstindex(s) + 3, lastindex(s) + 3)
5353
@test rpad("⟨k|H₁|k⟩", 12) |> textwidth == 12
5454
end
5555

56+
@testset "string truncation (ltrunc and rtrunc)" begin
57+
@test ltrunc("foo", 4) == "foo"
58+
@test ltrunc("foo", 3) == "foo"
59+
@test ltrunc("foo", 2) == "…o"
60+
@test ltrunc("🍕🍕 I love 🍕", 10) == "…I love 🍕" # handle wide emojis
61+
@test ltrunc("🍕🍕 I love 🍕", 10, "[…]") == "[…]love 🍕"
62+
# when the replacement string is longer than the trunc
63+
# trust that the user wants the replacement string rather than erroring
64+
@test ltrunc("abc", 2, "xxxxxx") == "xxxxxx"
65+
66+
@test rtrunc("foo", 4) == "foo"
67+
@test rtrunc("foo", 3) == "foo"
68+
@test rtrunc("foo", 2) == "f…"
69+
@test rtrunc("🍕🍕 I love 🍕", 10) == "🍕🍕 I lo…"
70+
@test rtrunc("🍕🍕 I love 🍕", 10, "[…]") == "🍕🍕 I […]"
71+
@test rtrunc("abc", 2, "xxxxxx") == "xxxxxx"
72+
73+
@test ctrunc("foo", 4) == "foo"
74+
@test ctrunc("foo", 3) == "foo"
75+
@test ctrunc("foo", 2) == "f…"
76+
@test ctrunc("foo", 2; prefer_left=true) == "f…"
77+
@test ctrunc("foo", 2; prefer_left=false) == "…o"
78+
@test ctrunc("foobar", 6) == "foobar"
79+
@test ctrunc("foobar", 5) == "fo…ar"
80+
@test ctrunc("foobar", 4) == "fo…r"
81+
@test ctrunc("🍕🍕 I love 🍕", 10) == "🍕🍕 …e 🍕"
82+
@test ctrunc("🍕🍕 I love 🍕", 10, "[…]") == "🍕🍕[…] 🍕"
83+
@test ctrunc("abc", 2, "xxxxxx") == "xxxxxx"
84+
end
85+
5686
# string manipulation
5787
@testset "lstrip/rstrip/strip" begin
5888
@test strip("") == ""

0 commit comments

Comments
 (0)