Skip to content

Commit

Permalink
Add the ability to omit text from both ends
Browse files Browse the repository at this point in the history
  • Loading branch information
piotrmurach committed Feb 21, 2021
1 parent 40671d7 commit 2c059ff
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 25 deletions.
48 changes: 45 additions & 3 deletions lib/strings/truncation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ def truncate(text, truncate_at = configuration.length, length: nil,
truncate_start(text, truncate_at, omission, separator)
when :middle
truncate_middle(text, truncate_at, omission, separator)
when :ends
truncate_ends(text, truncate_at, omission, separator)
when :end
truncate_from(0, text, truncate_at, omission, separator)
when Numeric
Expand Down Expand Up @@ -165,7 +167,9 @@ def truncate_start(text, length, omission, separator)

from = [text_width - length, 0].max
from += omission_width if from > 0
words, = *slice(text, from, length - omission_width, separator: separator)
words, = *slice(text, from, length - omission_width,
omission_width: omission_width,
separator: separator)

"#{omission if from > 0}#{words}"
end
Expand All @@ -192,6 +196,7 @@ def truncate_from(from, text, length, omission, separator)
length_without_omission = length - omission_width
length_without_omission -= omission_width if from > 0
words, stop = *slice(text, from, length_without_omission,
omission_width: omission_width,
separator: separator)

"#{omission if from > 0}#{words}#{omission if stop}"
Expand Down Expand Up @@ -224,30 +229,65 @@ def truncate_middle(text, length, omission, separator)
rem_omission = half_omission + omission_width % 2

before_words, = *slice(text, 0, half_length - half_omission,
omission_width: half_omission,
separator: separator)

after_words, = *slice(text, text_width - rem_length + rem_omission,
rem_length - rem_omission,
omission_width: rem_omission,
separator: separator)

"#{before_words}#{omission}#{after_words}"
end

# Truncate text at both ends
#
# @param [String] text
# the text to truncate
# @param [Integer] length
# the maximum length to truncate at
# @param [String] omission
# the string to denote omitted content
# @param [String|Regexp] separator
# the pattern or string to separate on
#
# @return [String]
# the truncated text
#
# @api private
def truncate_ends(text, length, omission, separator)
text_width = display_width(Strings::ANSI.sanitize(text))
omission_width = display_width(omission)
return text if text_width <= length
return omission if length <= 2 * omission_width

from = (text_width - length) / 2 + omission_width
words, stop = *slice(text, from, length - 2 * omission_width,
omission_width: omission_width,
separator: separator)
return omission if words.empty?

"#{omission if from > 0}#{words}#{omission if stop}"
end

# Extract number of characters from a text starting at the from position
#
# @param [Integer] from
# the position to start from
# @param [Integer] length
# the number of characters to extract
# @param [Integer] omission_width
# the width of the omission
# @param [String|Regexp] separator
# the string or pattern to use for splitting words
#
# @return [Array<String, Boolean>]
# return a substring and a stop flag
#
# @api private
def slice(text, from, length, separator: nil)
def slice(text, from, length, omission_width: 0, separator: nil)
scanner = StringScanner.new(text)
length_with_omission = length + omission_width
current_length = 0
start_position = 0
ansi_reset = false
Expand Down Expand Up @@ -284,13 +324,15 @@ def slice(text, from, length, separator: nil)
if char =~ separator
if word_break
word_break = false
current_length = 0
next
end
words << word.join
word.clear
end

if current_length <= length || scanner.check(END_REGEXP)
if current_length <= length || scanner.check(END_REGEXP) &&
current_length <= length_with_omission
if separator
word << char unless word_break
else
Expand Down
81 changes: 59 additions & 22 deletions spec/unit/truncate_ansi_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,28 +65,28 @@

context "from the end" do
[
["aaaaabbbbb", "", 0],
["aaaaabbbbb", "…", 1],
["aaaaabbbbb", "a…", 2],
["aaaaabbbbb", "aaaa…", 5],
["aaaaabbbbb", "aaaaa…", 6],
["aaaaabbbbb", "aaaaabbbbb", 10],
["aaaaabbbbb", "aa...", 5, { omission: "..." }],
["aaaaabbbbb", "aaa...", 6, { omission: "..." }],
["aaaaabbbbbc", "a…", 2],
["aaaaabbbbbc", "aaaa…", 5],
["aaaaabbbbbc", "aaaaa…", 6],
["aaaaabbbbbc", "aaaaabbbbbc", 11],
["aaaaabbbbbc", "aa...", 5, { omission: "..." }],
["aaaaabbbbbc", "aaa...", 6, { omission: "..." }],
["\e[34maaa bbb ccc\e[0m", "\e[34m\e[0m…", 1, {separator: " "}],
["\e[34maaa bbb ccc\e[0m", "\e[34m\e[0m…", 2, {separator: " "}],
["\e[34maaa bbb ccc\e[0m", "\e[34m\e[0m…", 3, {separator: " "}],
["\e[34maaa bbb ccc\e[0m", "\e[34maaa\e[0m…", 4, {separator: " "}],
["\e[34maaa bbb ccc\e[0m", "\e[34maaa\e[0m…", 5, {separator: " "}],
["\e[34maaa bbb ccc\e[0m", "\e[34maaa\e[0m…", 6, {separator: " "}],
["\e[34maaa bbb ccc\e[0m", "\e[34maaa\e[0m…", 7, {separator: " "}],
["\e[34maaa bbb ccc\e[0m", "\e[34maaa bbb\e[0m…", 8, {separator: " "}]
["\e[34maaaaabbbbb\e[0m", "", 0],
["\e[34maaaaabbbbb\e[0m", "\e[34m\e[0m…", 1],
["\e[34maaaaabbbbb\e[0m", "\e[34ma\e[0m…", 2],
["\e[34maaaaabbbbb\e[0m", "\e[34maaaa\e[0m…", 5],
["\e[34maaaaabbbbb\e[0m", "\e[34maaaaa\e[0m…", 6],
["\e[34maaaaabbbbb\e[0m", "\e[34maaaaabbbbb\e[0m", 10],
["\e[34maaaaabbbbb\e[0m", "\e[34maa\e[0m...", 5, { omission: "..." }],
["\e[34maaaaabbbbb\e[0m", "\e[34maaa\e[0m...", 6, { omission: "..." }],
["\e[34maaaaabbbbbc\e[0m", "\e[34ma\e[0m…", 2],
["\e[34maaaaabbbbbc\e[0m", "\e[34maaaa\e[0m…", 5],
["\e[34maaaaabbbbbc\e[0m", "\e[34maaaaa\e[0m…", 6],
["\e[34maaaaabbbbbc\e[0m", "\e[34maaaaabbbbbc\e[0m", 11],
["\e[34maaaaabbbbbc\e[0m", "\e[34maa\e[0m...", 5, { omission: "..." }],
["\e[34maaaaabbbbbc\e[0m", "\e[34maaa\e[0m...", 6, { omission: "..." }],
["\e[34maaa bbb ccc\e[0m", "\e[34m\e[0m…", 1, { separator: " " }],
["\e[34maaa bbb ccc\e[0m", "\e[34m\e[0m…", 2, { separator: " " }],
["\e[34maaa bbb ccc\e[0m", "\e[34m\e[0m…", 3, { separator: " " }],
["\e[34maaa bbb ccc\e[0m", "\e[34maaa\e[0m…", 4, { separator: " " }],
["\e[34maaa bbb ccc\e[0m", "\e[34maaa\e[0m…", 5, { separator: " " }],
["\e[34maaa bbb ccc\e[0m", "\e[34maaa\e[0m…", 6, { separator: " " }],
["\e[34maaa bbb ccc\e[0m", "\e[34maaa\e[0m…", 7, { separator: " " }],
["\e[34maaa bbb ccc\e[0m", "\e[34maaa bbb\e[0m…", 8, { separator: " " }]
].each do |text, truncated, length, options = {}|
it "truncates #{text.inspect} at #{length} -> #{truncated.inspect}" do
strings = Strings::Truncation.new
Expand All @@ -96,6 +96,43 @@
end
end

context "from both ends" do
[
["\e[34maaaaabbbbb\e[0m", "", 0],
["\e[34maaaaabbbbb\e[0m", "…", 1],
["\e[34maaaaabbbbb\e[0m", "…", 2],
["\e[34maaaaabbbbb\e[0m", "…\e[34ma\e[0m…", 3],
["\e[34maaaaabbbbb\e[0m", "…\e[34mab\e[0m…", 4],
["\e[34maaaaabbbbb\e[0m", "…\e[34maab\e[0m…", 5],
["\e[34maaaaabbbbb\e[0m", "…\e[34maabb\e[0m…", 6],
["\e[34maaaaabbbbb\e[0m", "\e[34maaaaabbbbb\e[0m", 10],
["\e[34maaaaabbbbb\e[0m", "...", 5, { omission: "..." }],
["\e[34maaaaabbbbb\e[0m", "...\e[34ma\e[0m...", 7, { omission: "..." }],
["\e[34maaaaabbbbb\e[0m", "...\e[34mab\e[0m...", 8, { omission: "..." }],
["\e[34maaaaabbbbbc\e[0m", "…", 2],
["\e[34maaaaabbbbbc\e[0m", "…\e[34mabb\e[0m…", 5],
["\e[34maaaaabbbbbc\e[0m", "…\e[34maabb\e[0m…", 6],
["\e[34maaaaabbbbbc\e[0m", "\e[34maaaaabbbbbc\e[0m", 11],
["\e[34maaaaabbbbbc\e[0m", "...", 5, { omission: "..." }],
["\e[34maaaaabbbbbc\e[0m", "...\e[34mb\e[0m...", 7, { omission: "..." }],
["\e[34maaaaabbbbbc\e[0m", "...\e[34mab\e[0m...", 8, { omission: "..." }],
["\e[34maaa bbb ccc\e[0m", "…", 1, { separator: " " }],
["\e[34maaa bbb ccc\e[0m", "…", 2, { separator: " " }],
["\e[34maaa bbb ccc\e[0m", "…\e[34m\e[0m…", 3, { separator: " " }],
["\e[34maaa bbb ccc\e[0m", "…\e[34m\e[0m…", 4, { separator: " " }],
["\e[34maaa bbb ccc\e[0m", "…\e[34mbbb\e[0m…", 5, { separator: " " }],
["\e[34maaa bbb ccc\e[0m", "…\e[34mbbb\e[0m…", 6, { separator: " " }],
["\e[34maaa bbb ccc\e[0m", "…\e[34mbbb\e[0m…", 7, { separator: " " }],
["\e[34maaa bbb ccc\e[0m", "…\e[34mbbb ccc\e[0m", 10, { separator: " " }]
].each do |text, truncated, length, options = {}|
it "truncates #{text.inspect} at #{length} -> #{truncated.inspect}" do
strings = Strings::Truncation.new
options.update(position: :ends)
expect(strings.truncate(text, length, **options)).to eq(truncated)
end
end
end

context "from the middle" do
[
["\e[34maaaaabbbbb\e[0m", "", 0],
Expand Down
36 changes: 36 additions & 0 deletions spec/unit/truncate_multibyte_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,42 @@
end
end

context "from both ends" do
[
["ありがとう", "", 0],
["ありがとう", "…", 1],
["ありがとう", "…", 2],
["ありがとう", "…が…", 5],
["ありがとう", "…がと…", 6],
["ありがとう", "…りが…", 7],
["ありがとう", "…りがと…", 8],
["ありがとう", "…りがとう", 9],
["ありがとう", "ありがとう", 10],
["ありがとう", "...", 7, { omission: "..." }],
["ありがとう", "...が...", 8, { omission: "..." }],
["ありがとう!", "…", 2],
["ありがとう!", "…が…", 5],
["ありがとう!", "…がと…", 6],
["ありがとう!", "…りがと…", 8],
["ありがとう!", "…りがとう!", 10],
["ありがとう!", "ありがとう!", 11],
["ありがとう!", "..が..", 7, { omission: ".." }],
["ありがとう!", "..がと..", 8, { omission: ".." }],
["あり がと う", "…", 1, { separator: " " }],
["あり がと う", "…", 2, { separator: " " }],
["あり がと う", "…がと…", 6, { separator: " " }],
["あり がと う", "…がと …", 7, { separator: " " }],
["あり がと う", "…がと う", 8, { separator: " " }],
["あり がと う", "…がと う", 9, { separator: " " }]
].each do |text, truncated, length, options = {}|
it "truncates #{text.inspect} at #{length} -> #{truncated.inspect}" do
strings = Strings::Truncation.new
options.update(position: :ends)
expect(strings.truncate(text, length, **options)).to eq(truncated)
end
end
end

context "from the middle" do
[
["ありがとう", "", 0],
Expand Down
50 changes: 50 additions & 0 deletions spec/unit/truncate_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,56 @@
end
end

context "from both ends" do
[
["aaaaabbbbb", "", 0],
["aaaaabbbbb", "…", 1],
["aaaaabbbbb", "…", 2],
["aaaaabbbbb", "…a…", 3],
["aaaaabbbbb", "…ab…", 4],
["aaaaabbbbb", "…aab…", 5],
["aaaaabbbbb", "…aabb…", 6],
["aaaaabbbbb", "aaaaabbbbb", 10],
["aaaaabbbbb", "...", 5, { omission: "..." }],
["aaaaabbbbb", "...a...", 7, { omission: "..." }],
["aaaaabbbbb", "...ab...", 8, { omission: "..." }],
["aaaaabbbbbc", "…", 2],
["aaaaabbbbbc", "…b…", 3],
["aaaaabbbbbc", "…ab…", 4],
["aaaaabbbbbc", "…abb…", 5],
["aaaaabbbbbc", "…aabb…", 6],
["aaaaabbbbbc", "aaaaabbbbbc", 11],
["aaaaabbbbbc", "...", 5, { omission: "..." }],
["aaaaabbbbbc", "...b...", 7, { omission: "..." }],
["aaaaabbbbbc", "...ab...", 8, { omission: "..." }],
["aaa bbb ccc", "…", 1, { separator: " " }],
["aaa bbb ccc", "…", 2, { separator: " " }],
["aaa bbb ccc", "…", 3, { separator: " " }],
["aaa bbb ccc", "…", 4, { separator: " " }],
["aaa bbb ccc", "…bbb…", 5, { separator: " " }],
["aaa bbb ccc", "…bbb…", 6, { separator: " " }],
["aaa bbb ccc", "…bbb…", 7, { separator: " " }],
["aaa bbb ccc", "…bbb ccc", 8, { separator: " " }],
["aaa bbb ccc", "..bbb..", 9, { separator: " ", omission: ".." }],
["aaa bbb ccc", "..bbb ccc", 10, { separator: " ", omission: ".." }]
].each do |text, truncated, length, options = {}|
it "truncates #{text.inspect} at #{length} -> #{truncated.inspect}" do
strings = Strings::Truncation.new
options.update(position: :ends)
expect(strings.truncate(text, length, **options)).to eq(truncated)
end
end

it "truncates text from both ends with long omission and separator" do
text = "It is not down on any map; true places never are."
truncation = Strings::Truncation.truncate(text, 35, position: :ends,
omission: "...",
separator: " ")

expect(truncation).to eq("...down on any map; true places...")
end
end

context "from the middle" do
[
["aaaaabbbbb", "", 0],
Expand Down

0 comments on commit 2c059ff

Please sign in to comment.