class String

def truncate_bytes(truncate_to, omission: "…")

Raises +ArgumentError+ when the bytesize of :omission exceeds truncate_to.

to "…", for a total length not exceeding truncate_to.
The truncated text ends with the :omission string, defaulting

=> "🔪🔪🔪🔪…"
>> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".truncate_bytes(20)
=> 80
>> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".bytesize
=> 20
>> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".size

characters.
grapheme clusters ("perceptual characters") by truncating at combining
breaking string encoding by splitting multibyte characters or breaking
Truncates +text+ to at most truncate_to bytes in length without
def truncate_bytes(truncate_to, omission: "…")
  omission ||= ""
  case
  when bytesize <= truncate_to
    dup
  when omission.bytesize > truncate_to
    raise ArgumentError, "Omission #{omission.inspect} is #{omission.bytesize}, larger than the truncation length of #{truncate_to} bytes"
  when omission.bytesize == truncate_to
    omission.dup
  else
    self.class.new.tap do |cut|
      cut_at = truncate_to - omission.bytesize
      each_grapheme_cluster do |grapheme|
        if cut.bytesize + grapheme.bytesize <= cut_at
          cut << grapheme
        else
          break
        end
      end
      cut << omission
    end
  end
end