Многие символы в этом диапазоне не могут быть напечатаны (как вы уже заметили) или являются суррогатными, пользовательскими или иным недопустимыми символами.Лучший подход (который я могу придумать) состоит в том, чтобы сгенерировать последовательность символов, проверить каждый из них, чтобы убедиться, что он действителен и пригоден для печати, а затем взять первые 20 из них.
Несколько заметок.Мы хотим сделать rand(0x10000)
в этом случае, а не rand(0xFFFF)
, потому что Random#rand
и Kernel#rand
вернут число, меньшее, чем его аргумент, и вы хотите включить U + FFFF в вашу выборку.Мы также должны дать себе некоторую гибкость для выполнения однобайтового, двухбайтового, трехбайтового или четырехбайтового UTF-8.
Давайте начнем с базового генератора последовательностей, называемого Enumerator в Ruby.Этот объект выдает значения по одному и может представлять конечную или бесконечную последовательность.В этом случае мы хотим перечислить бесконечную последовательность случайных трехбайтовых символов UTF-8, пропуская недопустимые символы по мере продвижения.
random_utf8 = Enumerator.new do |yielder|
loop do
yielder << rand(0x10000).chr('UTF-8')
rescue RangeError
end
end
Вы можете извлечь значения из Enumerator с помощью #next
, чтобы увидеть его в действии:
irb(main):007:0> random_utf8.next
=> "\u9FEB"
irb(main):008:0> random_utf8.next
=> "槇"
irb(main):009:0> random_utf8.next
=> "엛"
(вы заметите, что один из них не "рендерится""потому что это не печатный символ. Это иллюстрирует, почему нам нужно отфильтровать значения перед тем, как выбрать 20 из них.)
Теперь мы можем снять символы с этой последовательности и проверить каждый из них, чтобы убедиться, что она печатается.Единственный улов в том, что мы хотим сделать это лениво , чтобы избежать проверки каждого символа в бесконечной последовательности (что невозможно), прежде чем перейти к следующему шагу в цепочке.Наконец, мы возьмем первые 20 печатных символов и соединим их вместе в строку.
random_utf8
.lazy
.grep(/[[:print:]]/) # or [[:alpha:]] or \p{L} or whatever test you want here
.first(20)
.join # => "醸긍ᅋꝇ꼏捁㨃농鳹䝛ㆅ⇂擒璝缀챼砶"
Теперь давайте абстрагируем это в метод, чтобы мы могли параметризовать некоторые вещи.Ruby дает нам отличный способ вернуть Enumerator из метода, который возвращает значения, возвращая Object#enum_for
(он же Object#to_enum
) с символом метода и любые другие аргументы, передаваемые функции.
def random_utf8(mb=3)
return enum_for(__callee__, mb) unless block_given?
# determine the maximum codepoint based on the number of UTF-8 bytes
max = [0x80, 0x800, 0x10000, 0x110000][mb.pred]
loop do
yield rand(max).chr('UTF-8') # note the `yield` here
rescue RangeError
end
end
Мы можем использовать этот метод точно так же, как мы использовали наш перечислитель выше, опционально передавая желаемое количество байтов UTF-8.
Этот подход также дает нам возможностьвызовите наш метод с блоком вместо цепочки операций за ним:
random_utf8(2) do |char|
next unless char.match?(/[[:print:]]/)
puts "Got >#{char}<!"
break # don't loop infinitely
end
Что, по общему признанию, не очень полезно в данном конкретном случае.
Еще одно примечание о реализации этогоРешение: Вы можете легко переместить печатаемый чек в тело метода или переместить обработку исключений RangeError из тела метода.Вы также можете заставить метод возвращать ленивый Enumerator по умолчанию.Это действительно зависит от вас, чтобы разработать метод с учетом требований вашего приложения.
def lazy_printable_random_utf8(mb=3)
return enum_for(__callee__, mb).lazy unless block_given?
# determine the maximum codepoint based on the number of UTF-8 bytes
max = [0x80, 0x800, 0x10000, 0x110000][mb.pred]
loop do
char = rand(max).chr('UTF-8')
yield char if char.match?(/[[:print:]]/)
rescue RangeError
end
end