Ruby: ограничение строки UTF-8 по байтам

На этой странице RabbitMQ указано:

Имена очереди могут содержать до 255 байтов символов UTF-8.

В ruby ​​(1.9.3), как бы я обрезал строку UTF-8 байтом, не разбивая середину символа? Результирующая строка должна быть самой длинной допустимой строкой UTF-8, которая соответствует предельному байту.

Ответ 1

Я думаю, что нашел что-то, что работает.

def limit_bytesize(str, size)
  str.encoding.name == 'UTF-8' or raise ArgumentError, "str must have UTF-8 encoding"

  # Change to canonical unicode form (compose any decomposed characters).
  # Works only if you're using active_support
  str = str.mb_chars.compose.to_s if str.respond_to?(:mb_chars)

  # Start with a string of the correct byte size, but
  # with a possibly incomplete char at the end.
  new_str = str.byteslice(0, size)

  # We need to force_encoding from utf-8 to utf-8 so ruby will re-validate
  # (idea from halfelf).
  until new_str[-1].force_encoding('utf-8').valid_encoding?
    # remove the invalid char
    new_str = new_str.slice(0..-2)
  end
  new_str
end

Использование:

>> limit_bytesize("abc\u2014d", 4)
=> "abc"
>> limit_bytesize("abc\u2014d", 5)
=> "abc"
>> limit_bytesize("abc\u2014d", 6)
=> "abc—"
>> limit_bytesize("abc\u2014d", 7)
=> "abc—d"

Обновление...

Развернутое поведение без active_support:

>> limit_bytesize("abc\u0065\u0301d", 4)
=> "abce"
>> limit_bytesize("abc\u0065\u0301d", 5)
=> "abce"
>> limit_bytesize("abc\u0065\u0301d", 6)
=> "abcé"
>> limit_bytesize("abc\u0065\u0301d", 7)
=> "abcéd"

Разложимое поведение с active_support:

>> limit_bytesize("abc\u0065\u0301d", 4)
=> "abc"
>> limit_bytesize("abc\u0065\u0301d", 5)
=> "abcé"
>> limit_bytesize("abc\u0065\u0301d", 6)
=> "abcéd"

Ответ 2

Для Rails >= 3.0 у вас есть метод ActiveSupport:: Multibyte:: Chars limit.

Из документов API:

- (Object) limit(limit) 

Ограничить размер байта строки количеством байтов без нарушения символов. Используется, когда хранилище для строки ограничено по какой-либо причине.

Пример:

'こんにちは'.mb_chars.limit(7).to_s # => "こん"

Ответ 3

bytesize даст вам длину строки в байтах while (до тех пор, пока строковая кодировка будет установлена ​​правильно), такие как slice не будут калечить строку.

Простым процессом было бы просто перебрать строку

s.each_char.each_with_object('') do|char, result| 
  if result.bytesize + char.bytesize > 255
    break result
  else
    result << char
  end
end

Если вы были хитрым, вы скопировали первые 63 символа напрямую, так как любой символ юникода не более 4 байтов в utf-8.

Обратите внимание, что это все еще не идеально. Например, представьте, что последние 4 байта вашей строки являются символами "e" и сочетают острый акцент. Нарезание последних 2 байтов создает строку, которая все еще является utf8, но с точки зрения того, что видит пользователь, изменит вывод с 'é' на 'e', ​​что может изменить смысл текста. Это, вероятно, не огромная сделка, когда вы просто называете очереди RabbitMQ, но могут быть важны в других обстоятельствах. Например, на французском языке заголовок информационного бюллетеня "Un policier tué" означает "Полицейский был убит", тогда как "Un policier tue" означает "Убийца полицейских".

Ответ 4

Как насчет этого:

s = "δogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδog"
count = 0
while true
  more_truncate = "a" + (255-count).to_s
  s2 = s.unpack(more_truncate)[0]
  s2.force_encoding 'utf-8'

  if s2[-1].valid_encoding?
    break
  else
    count += 1
  end
end

s2.force_encoding 'utf-8'
puts s2

Ответ 5

Rails 6 предоставит String # truncate_bytes, который ведет себя как truncate, но принимает количество байтов вместо числа символов. И, конечно, он возвращает допустимую строку (она не вырезает вслепую в середине многобайтового символа).

Взято из документа:

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