From dd5c1b2ec42d3a2b923bbe46f4b17dcb3c660582 Mon Sep 17 00:00:00 2001 From: Daniel Rizk Date: Thu, 22 Aug 2024 09:59:52 -0400 Subject: [PATCH] adds extract, pad, others, fixes fxns acting on vecs that shd be strs --- README.md | 40 ++------ docs/src/index.md | 40 ++------ src/TidierStrings.jl | 5 +- src/characters.jl | 33 ++++++- src/concatenation.jl | 18 +++- src/docstrings.jl | 215 +++++++++++++++++++++++++++++++++++-------- src/matching.jl | 82 +++++++++++------ src/other.jl | 37 +++++++- 8 files changed, 329 insertions(+), 141 deletions(-) diff --git a/README.md b/README.md index 6c62510..5f7e811 100644 --- a/README.md +++ b/README.md @@ -36,37 +36,15 @@ Pkg.add(url = "https://github.com/TidierOrg/TidierStrings.jl.git") TidierStrings.jl currently supports: -- `str_detect()` -- `str_replace()` -- `str_replace_all()` -- `str_replace_missing()` -- `str_removal_all()` -- `str_remove()` -- `str_count()` -- `str_squish()` -- `str_equal()` -- `str_to_upper()` -- `str_to_lower()` -- `str_to_title()` -- `str_to_sentence()` -- `str_c` -- `str_dup()` -- `str_length()` -- `str_width()` -- `str_trim()` -- `str_subset()` -- `str_unique()` -- `str_starts()` -- `str_ends()` -- `str_which()` -- `str_flatten()` -- `str_flatten_comma()` -- `str_locate()` -- `str_locate_all()` -- `str_conv` -- `str_like` -- `str_wrap` -- `word()` +| **Category** | **Function** | +|-------------------|----------------------------------------------------------------------------------------------------| +| **Matching** | `str_count`, `str_detect`, `str_locate`, `str_locate_all`, `str_replace`, `str_replace_all`, | +| | `str_remove`, `str_remove_all`, `str_split`, `str_starts`, `str_ends`, `str_subset`, `str_which` | +| **Concatenation** | `str_c`, `str_flatten`, `str_flatten_comma` | +| **Characters** | `str_dup`, `str_length`, `str_width`, `str_trim`, `str_squish`, `str_wrap`, `str_pad` | +| **Locale** | `str_equal`, `str_to_upper`, `str_to_lower`, `str_to_title`, `str_to_sentence`, `str_unique` | +| **Other** | `str_conv`, `str_like`, `str_replace_missing`, `word` | + ## Examples diff --git a/docs/src/index.md b/docs/src/index.md index 9044749..3907b88 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -6,35 +6,11 @@ The goal of this package is to replicate the beauty of stringr from R in Julia i This package includes: - -- `str_detect()` -- `str_replace()` -- `str_replace_all()` -- `str_replace_missing()` -- `str_removal_all()` -- `str_remove()` -- `str_count()` -- `str_squish()` -- `str_equal()` -- `str_to_upper()` -- `str_to_lower()` -- `str_to_title()` -- `str_to_sentence()` -- `str_c` -- `str_dup()` -- `str_length()` -- `str_width()` -- `str_trim()` -- `str_subset()` -- `str_unique()` -- `str_starts()` -- `str_ends()` -- `str_which()` -- `str_flatten()` -- `str_flatten_comma()` -- `str_locate()` -- `str_locate_all()` -- `str_conv` -- `str_like` -- `str_wrap` -- `word()` +| **Category** | **Function** | +|-------------------|----------------------------------------------------------------------------------------------------| +| **Matching** | `str_count`, `str_detect`, `str_locate`, `str_locate_all`, `str_replace`, `str_replace_all`, | +| | `str_remove`, `str_remove_all`, `str_split`, `str_starts`, `str_ends`, `str_subset`, `str_which` | +| **Concatenation** | `str_c`, `str_flatten`, `str_flatten_comma` | +| **Characters** | `str_dup`, `str_length`, `str_width`, `str_trim`, `str_squish`, `str_wrap`, `str_pad` | +| **Locale** | `str_equal`, `str_to_upper`, `str_to_lower`, `str_to_title`, `str_to_sentence`, `str_unique` | +| **Other** | `str_conv`, `str_like`, `str_replace_missing`, `word` | diff --git a/src/TidierStrings.jl b/src/TidierStrings.jl index 7a90049..13faf0a 100644 --- a/src/TidierStrings.jl +++ b/src/TidierStrings.jl @@ -6,14 +6,15 @@ export # matching str_count, str_detect, str_locate, str_locate_all, str_replace, str_replace_all, str_remove, str_remove_all, str_split, str_starts, str_ends, str_subset, str_which, + str_extract, str_extract_all, str_subset, # concatenation str_c, str_flatten, str_flatten_comma, # characters - str_dup, str_length, str_width, str_trim, str_squish, str_wrap, + str_dup, str_length, str_width, str_trim, str_squish, str_wrap, str_pad, # locale str_equal, str_to_upper, str_to_lower, str_to_title, str_to_sentence, str_unique, # other - str_conv, str_like, str_replace_missing, word + str_conv, str_like, str_replace_missing, word, str_trunc include("docstrings.jl") include("matching.jl") diff --git a/src/characters.jl b/src/characters.jl index 9aa4f85..843c7fa 100644 --- a/src/characters.jl +++ b/src/characters.jl @@ -86,4 +86,35 @@ function str_wrap( Base.push!(lines, current_line) return Base.join(lines, "\n") -end \ No newline at end of file +end + +""" +$docstring_str_pad +""" +function str_pad(string::AbstractString, width::Integer; + side::String = "right", pad::AbstractString = " ", use_width::Bool = true) + if use_width + len = length(string) + else + len = ncodeunits(string) + end + + if width <= len + return string + end + + padding_length = width - len + padding = repeat(pad, cld(padding_length, length(pad))) + + if side == "right" + return string * first(padding, padding_length) + elseif side == "left" + return first(padding, padding_length) * string + elseif side == "both" + left_pad = div(padding_length, 2) + right_pad = padding_length - left_pad + return first(padding, left_pad) * string * first(padding, right_pad) + else + throw(ArgumentError("side must be one of 'left', 'right', or 'both'")) + end +end diff --git a/src/concatenation.jl b/src/concatenation.jl index a428ec9..ee8f17c 100644 --- a/src/concatenation.jl +++ b/src/concatenation.jl @@ -1,10 +1,20 @@ """ $docstring_str_c """ -function str_c(strings::AbstractVector; sep::AbstractString="") - strings = filter(!ismissing, strings) - - return join(strings, sep) +function str_c(strings::AbstractVector...; sep::AbstractString = "", collapse::AbstractString = "") + # Remove any `missing` values from each vector + filtered_strings = [filter(!ismissing, vec) for vec in strings] + + if length(strings) == 1 + # If only one vector is provided, join its elements without a separator + return join(filtered_strings[1]) + else + # Zip the vectors together and concatenate corresponding elements with `sep` + concatenated = [join(pair, sep) for pair in zip(filtered_strings...)] + + # Join the concatenated elements with `collapse` + return collapse == "" ? concatenated : join(concatenated, collapse) + end end """ diff --git a/src/docstrings.jl b/src/docstrings.jl index 2534268..fabac47 100644 --- a/src/docstrings.jl +++ b/src/docstrings.jl @@ -1,11 +1,11 @@ const docstring_str_detect = """ - str_detect(column::String, pattern::Union{String, Regex}) + str_detect(string::String, pattern::Union{String, Regex}) Determine if a string contains a certain pattern. # Arguments -- `column`: The string to check. +- `string`: The string to check. pattern: A string or a regular expression to find within the string. The pattern can include special logic: @@ -95,12 +95,12 @@ julia> str_flatten_comma(['a', 'b'], " and ") const docstring_str_replace = """ - str_replace(column::String, pattern::Union{String, Regex}, replacement::String) + str_replace(string::String, pattern::Union{String, Regex}, replacement::String) Replace the first occurrence of a pattern in a string with a specified string. Arguments -column: The string in which to replace the pattern. +string: The string in which to replace the pattern. pattern: A string or a regular expression to find within the string. replacement: The string to insert in place of the pattern. The pattern can include special logic: @@ -110,6 +110,9 @@ Returns A new string with the first occurrence of the pattern replaced with the replacement. Examples ```jldoctest +julia> str_replace("I Think You Should Leave is a great show", " ", "") +"IThink You Should Leave is a great show" + julia> str_replace("The sky is blue", "blue", "red") "The sky is red" @@ -123,12 +126,12 @@ julia> str_replace("The sky is blue", "blue|sky", "red") const docstring_str_replace_all = """ - str_replace_all(column::String, pattern::Union{String, Regex}, replacement::String) + str_replace_all(String::String, pattern::Union{String, Regex}, replacement::String) Replace all occurrences of a pattern in a string with a specified string. # Arguments -- `column`: The string in which to replace the pattern. +- `string`: The string in which to replace the pattern. - `pattern`: A string or a regular expression to find within the string. replacement: The string to insert in place of the pattern. The pattern can include special logic: @@ -138,6 +141,9 @@ Returns A new string with all occurrences of the pattern replaced with the replacement. # Examples ```jldoctest +julia> str_replace_all("I Think You Should Leave is a great show", " ", "") +"IThinkYouShouldLeaveisagreatshow" + julia> str_replace_all("The blue sky is blue", "blue", "red") "The red sky is red" @@ -150,18 +156,18 @@ julia> str_replace_all("The blue sky is blue", "blue|sky", "red") """ const docstring_str_count = """ - str_count(column::String, pattern::Union{String, Regex}) + str_count(string::String, pattern::Union{String, Regex}) Count the number of non-overlapping occurrences of a pattern in a string. # Arguments -- `column`: The string in which to count the pattern. +- `string`: The string in which to count the pattern. - `pattern`: A string or a regular expression to find within the string. The pattern can include special logic: Use | to represent "or" (e.g., "red|blue" counts any string that contains "red" or "blue"). Returns -The count of non-overlapping occurrences of pattern in column. +The count of non-overlapping occurrences of pattern in string. Examples ```jldoctest julia> str_count("The blue sky is blue", "blue") @@ -177,14 +183,14 @@ julia> str_count("The blue sky is blue", "blue|sky") const docstring_str_squish = """ - str_squish(column::String) + str_squish(string::String) Squish a string, removing consecutive whitespace and replacing it with a single space, as well as removing leading and trailing whitespace. #Arguments -`column`: The string to be squished. +`string`: The string to be squished. Returns -A squished version of column. +A squished version of string. # Examples ```jldoctest julia> str_squish(" This is a string with spaces ") @@ -198,15 +204,15 @@ julia> str_squish(" Leading and trailing spaces ") const docstring_str_equal = """ - str_equal(column::String, pattern::Union{String, Regex}) + str_equal(string::String, pattern::Union{String, Regex}) Check if a string exactly equals to a pattern, or for regular expressions, if the pattern can match the entire string. # Arguments -- `column`: The string to be checked. +- `string`: The string to be checked. - `pattern`: The pattern to compare against. Can be a plain string or a Regex. Returns -true if column equals to pattern (for plain strings) or if pattern can match the entire column (for Regex). +true if string equals to pattern (for plain strings) or if pattern can match the entire string (for Regex). false otherwise. # Examples ```jldoctest @@ -217,12 +223,12 @@ true const docstring_str_subset = """ - str_subset(column::String, pattern::Union{String, Regex}) + str_subset(string::String, pattern::Union{String, Regex}) Subset a string based on the presence of pattern. If the pattern exists within the string, the function will return the original string. If the pattern is not found within the string, the function will return an empty string. # Arguments -- `column`: The string from which to extract the subset. +- `string`: The string from which to extract the subset. - `pattern`: The pattern to search for within the string. Can be a plain string or a Regex. Returns The original string if the pattern is found within it, otherwise an empty string. @@ -274,12 +280,12 @@ julia> str_to_upper("Hello World!") const docstring_str_remove_all = """ - str_remove_all(column::String, pattern::Union{String, Regex}) + str_remove_all(string::String, pattern::Union{String, Regex}) Remove all occurrences of the pattern in the string. # Arguments -- `column`: The string from which the pattern should be removed. +- `string`: The string from which the pattern should be removed. - `pattern`: The pattern which should be removed from the string. Can be a string or a regular expression. Returns @@ -287,22 +293,22 @@ A string with all occurrences of the pattern removed. Examples ```jldoctest -julia> column = "I love tidier strings, I love tidier strings" +julia> string = "I love tidier strings, I love tidier strings" "I love tidier strings, I love tidier strings" -julia> str_remove_all(column, " strings") +julia> str_remove_all(string, " strings") "I love tidier , I love tidier " ``` """ const docstring_str_remove = """ - str_remove(column::String, pattern::Union{String, Regex}) + str_remove(string::String, pattern::Union{String, Regex}) Remove the first occurrence of the pattern in the string. Arguments -- `column`: The string from which the pattern should be removed. +- `string`: The string from which the pattern should be removed. - `pattern`: The pattern which should be removed from the string. Can be a string or a regular expression. Returns @@ -310,10 +316,10 @@ A string with the first occurrence of the pattern removed. Examples ```jldoctest -julia> column = "I love tidier strings strings" +julia> string = "I love tidier strings strings" "I love tidier strings strings" -julia> str_remove(column, " strings") +julia> str_remove(string, " strings") "I love tidier strings" ``` """ @@ -509,7 +515,7 @@ julia> word("Jane saw a cat", 2, -1) const docstring_str_starts = """ - str_starts(string::Vector{T}, pattern::Union{AbstractString, Regex}; negate::Bool=false) + str_starts(string::String, pattern::Union{AbstractString, Regex}; negate::Bool=false) Check if a string starts with a certain pattern. @@ -523,24 +529,26 @@ A vector of booleans indicating if the string starts with the pattern. Examples ```jldoctest -julia> str_starts(["apple", "banana", "pear", "pineapple"], r"^p") # [false, false, true, true] -4-element Vector{Bool}: +julia> str_starts.(["apple", "banana", "pear", "pineapple"], r"^p") # [false, false, true, true] +4-element BitVector: 0 0 1 1 -julia> str_starts(["apple", "banana", "pear", "pineapple"], r"^p", negate=true) # [true, true, false, false] -4-element Vector{Bool}: +julia> str_starts.(["apple", "banana", "pear", "pineapple"], r"^p", negate=true) # [true, true, false, false] +4-element BitVector: 1 1 0 0 +julia> str_starts("apple pineapple", r"^p") +false ``` """ const docstring_str_ends = """ - str_ends(string::Vector{T}, pattern::Union{AbstractString, Regex}; negate::Bool=false) + str_ends(string::String, pattern::Union{AbstractString, Regex}; negate::Bool=false) Check if a string ends with a certain pattern. @@ -554,14 +562,17 @@ A vector of booleans indicating if the string ends with the pattern. Examples ```jldoctest -julia> str_ends(["apple", "banana", "pear", "pineapple"], r"e\$") # [true, false, false, true] -4-element Vector{Bool}: +julia> str_ends("apple pineapple", r"^p") +false + +julia> str_ends.(["apple", "banana", "pear", "pineapple"], r"e\$") # [true, false, false, true] +4-element BitVector: 1 0 0 1 -julia> str_ends(["apple", "banana", "pear", "pineapple"], r"e\$", negate=true) # [false, true, true, false] -4-element Vector{Bool}: +julia> str_ends.(["apple", "banana", "pear", "pineapple"], r"e\$", negate=true) # [false, true, true, false] +4-element BitVector: 0 1 1 @@ -700,7 +711,7 @@ julia> str_conv([0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x20, 0x77, 0x6F, 0x72, 0x6 const docstring_str_like = """ - str_like(string::AbstractVector{String}, pattern::String; ignore_case::Bool = true) + str_like(string, pattern::String; ignore_case::Bool = true) Detect a pattern in each string of the input vector using SQL-like pattern matching. @@ -713,8 +724,11 @@ Returns A vector of booleans indicating if the string matches the pattern. ```jldoctest -julia> str_like(["Hello", "world", "HELLO", "WORLD"], "H_llo") -4-element Vector{Bool}: +julia> str_like("hello", "h_llo") +true + +julia> str_like.(["Hello", "world", "HELLO", "WORLD"], "H_llo") +4-element BitVector: 1 0 1 @@ -731,7 +745,7 @@ Joins a vector of strings into a single string. Arguments - `strings`: Input strings. - `sep`: The separator between the strings. Default is an empty string. - +- `collapse` : If provided, it joins the concatenated strings with the specified collapse string. If not, it returns an array of the concatenated strings. Returns The joined string. @@ -739,6 +753,14 @@ Examples ```jldoctest julia> str_c(["apple", "banana", "pear", "pineapple"]) "applebananapearpineapple" + +julia> str_c(["Michigan", "Maryland"] , ["MI", "MD"], sep = ", ") +2-element Vector{String}: + "Michigan, MI" + "Maryland, MD" + +julia> str_c(["Michigan", "Maryland"] , ["MI", "MD"], sep = ", ", collapse = "; ") +"Michigan, MI; Maryland, MD" ``` """ @@ -767,4 +789,119 @@ wrapped based on the given width and breaking rules. ``` +""" + + +const docstring_str_pad = +""" + str_pad(string::AbstractString, width::Integer; side::String="right", pad::AbstractString=" ", use_width::Bool=true) + +Pad a string to a certain width. + +# Returns +The padded string. + +# Arguments +- `string`: The string to be padded. +- `width`: The width to pad the string to. +- `side`: The side to pad the string on. Can be "left", "right", or "both". +- `pad`: The string to use for padding. +- `use_width`: Whether to use the width argument or the length of the string. + +# Examples +```jldoctest +julia> str_pad("hello", 10) +"hello " + +julia> str_pad("hello", 10, side="left") +" hello" + +julia> str_pad("hello", 10, side="both") +" hello " + +julia> str_pad("hello", 10, side="both", pad="*") +"**hello***" +``` +""" + +const docstring_str_extract_all = +""" +str_extract_all(strings, pattern::Union{String, Regex}) + +Extract all occurrences of a pattern from a string + +# Arguments +- `strings`: A string to search for matches. +- `pattern`: The pattern to search for, either as a String or a Regex. + +# Examples +```jldoctest +julia> str_extract_all.(["hello world", "hello universe hello", "goodbye"], r"hello") +3-element Vector{Union{Missing, Vector{String}}}: +["hello"] +["hello", "hello"] +missing + +julia> str_extract_all("hello world hello universe hello goodbye", r"hello") +3-element Vector{String}: + "hello" + "hello" + "hello" +""" + +const docstring_str_extract = +""" + str_extract(string, pattern::Union{String, Regex}) + +Extract the first occurrence of a pattern from a string + +# Arguments +- `strings`: A string to search for matches. +- `pattern`: The pattern to search for, either as a String or a Regex. + +# Examples +```jldoctest +julia> str_extract("hello world hello universe hello goodbye", r"hello") +"hello" + +julia> str_extract.(["hello world", "hello universe", "goodbye"], "hello") +3-element Vector{Union{Missing, String}}: + "hello" + "hello" + missing + ``` +""" + +const docstring_str_trunc = +""" +str_trunc(string::AbstractString, width::Integer; side::String="right", ellipsis::AbstractString="...") + +Truncate a string to a fixed number of characters. + +# Arguments +- `string`: Input string to be truncated. +- `width`: Maximum width of the resulting string, including the ellipsis. +- `side`: Side from which to truncate. Can be "right", "left", or "center". Defaults to "right". +- `ellipsis`: String to indicate content has been removed. Defaults to "...". + +# Returns +A truncated string of length less than or equal to `width`, including the ellipsis. + +# Examples +```jldoctest +julia> str_trunc("This is a long string", 10) +"This is..." + +julia> str_trunc("This is a long string", 10, side="left") +"...g string" + +julia> str_trunc("This is a long string", 10, side="center") +"Thi...ring" + +julia> str_trunc("Short", 10) +"Short" + +julia> str_trunc("This is a long string that needs to be truncated", 20, side = "right", ellipsis = "--") +"This is a long str--" +``` """ \ No newline at end of file diff --git a/src/matching.jl b/src/matching.jl index 64d7300..ad4ae14 100644 --- a/src/matching.jl +++ b/src/matching.jl @@ -111,28 +111,20 @@ end """ $docstring_str_replace_all """ -function str_replace_all(column, pattern::Union{String,Regex}, replacement::String) +function str_replace_all(column, pattern::Union{String, Regex}, replacement::String) if ismissing(column) - return (column) + return column end - if pattern isa String - patterns = split(pattern, '|') - for p in patterns - # Convert the pattern to a Regex object - regex = Regex(strip(p)) - column = replace(column, regex => replacement) - end - else - # For regular expressions, directly use replace - regex = Regex(pattern.pattern) - column = replace(column, regex => replacement) - end - # Replace multiple consecutive spaces with a single space - column = replace(column, r"\s+" => " ") - return column + regex_pattern = isa(pattern, String) ? Regex(pattern) : pattern + + column = replace(column, regex_pattern => replacement) + + return replace(column, r"\s+" => " ") end + + """ $docstring_str_remove """ @@ -199,33 +191,39 @@ end """ $docstring_str_starts """ -function str_starts(string::Vector{T}, pattern::Union{AbstractString,Regex}; negate::Bool=false)::Vector{Bool} where {T} +function str_starts(string::AbstractString, pattern::Union{AbstractString, Regex}; negate::Bool=false)::Bool + if ismissing(string) + return (string) + end if pattern isa Regex - matches = [match(pattern, s) !== nothing for s in string] - return negate ? .!matches : matches + match_result = match(pattern, string) !== nothing elseif pattern isa AbstractString - matches = [startswith(s, pattern) for s in string] - return negate ? .!matches : matches + match_result = startswith(string, pattern) else error("Pattern must be either a Regex or an AbstractString.") end + + return negate ? !match_result : match_result end - """ $docstring_str_ends """ -function str_ends(string::Vector{T}, pattern::Union{AbstractString,Regex}; negate::Bool=false)::Vector{Bool} where {T} +function str_ends(string::AbstractString, pattern::Union{AbstractString, Regex}; negate::Bool=false)::Bool + if ismissing(string) + return (string) + end if pattern isa Regex - matches = [match(pattern, s) !== nothing for s in string] - return negate ? .!matches : matches + match_result = match(pattern, string) !== nothing elseif pattern isa AbstractString - matches = [endswith(s, pattern) for s in string] - return negate ? .!matches : matches + match_result = endswith(string, pattern) else error("Pattern must be either a Regex or an AbstractString.") end + + return negate ? !match_result : match_result end + """ $docstring_str_subset """ @@ -275,3 +273,31 @@ function str_which(strings::Vector{T}, pattern::Union{AbstractString,Regex}; neg return indices end end + + +""" +$docstring_str_extract +""" +function str_extract(input::AbstractString, pattern::Union{String, Regex}) + # Convert pattern to Regex if it's a string + regex_pattern = isa(pattern, String) ? Regex(pattern) : pattern + + # Find the first match, return missing if none found + m = match(regex_pattern, input) + return m === nothing ? missing : m.match +end + +""" +$docstring_str_extract_all +""" +function str_extract_all(string::AbstractString, pattern::Union{String, Regex}) + # Convert pattern to Regex if it's a string + regex_pattern = isa(pattern, String) ? Regex(pattern) : pattern + + # Collect matches + matches = [String(m.match) for m in eachmatch(regex_pattern, string)] + + # Return missing if no matches found + return isempty(matches) ? missing : matches +end + diff --git a/src/other.jl b/src/other.jl index 09c0c0a..5b9ff51 100644 --- a/src/other.jl +++ b/src/other.jl @@ -14,7 +14,7 @@ end """ $docstring_str_like """ -function str_like(string::AbstractVector{String}, pattern::String; ignore_case::Bool=true) +function str_like(string::AbstractString, pattern::String; ignore_case::Bool=true) # Convert SQL LIKE pattern to Julia regex pattern julia_pattern = replace(pattern, r"[%_]" => s -> s == "%" ? ".*" : ".") julia_pattern = replace(julia_pattern, r"(\\%)" => "%") @@ -24,8 +24,8 @@ function str_like(string::AbstractVector{String}, pattern::String; ignore_case:: regex_flags = ignore_case ? "i" : "" regex_pattern = Regex("^" * julia_pattern * "\$", regex_flags) - # Apply the pattern to each string in the input vector - return [occursin(regex_pattern, str) for str in string] + # Apply the pattern to the input string + return occursin(regex_pattern, string) end """ @@ -54,4 +54,33 @@ function word(string::AbstractString, start_index::Int=1, end_index::Int=start_i end return words[start_index:end_index] -end \ No newline at end of file +end + +""" +$docstring_str_trunc +""" +function str_trunc(string::AbstractString, width::Int; side::String = "right", ellipsis::AbstractString = "...") + # Handle cases where truncation is not needed + if length(string) <= width + return string + end + + # Adjust width to account for ellipsis + adjusted_width = max(width - length(ellipsis), 0) + + truncated_string = "" + + # Truncate based on the side argument + if side == "right" + truncated_string = string[1:adjusted_width] * ellipsis + elseif side == "left" + truncated_string = ellipsis * string[end-adjusted_width:end] + elseif side == "center" + half_width = div(adjusted_width, 2) + truncated_string = string[1:half_width] * ellipsis * string[end-half_width:end] + else + throw(ArgumentError("Invalid value for side. Must be :right, :left, or :center.")) + end + + return truncated_string +end