いくつかの制限を設けた、初期値パスワードとして使うランダム文字列をユルく生成する。

要件

  • 一部のまぎらわしい文字を使用しない(今回は o, O, 0)
  • 同じ文字が3つ以上連続しない
  • 記号を1〜2つ含むよう指示するオプションを設ける
  • 最初と最後の文字には記号は使わない

制限

  • 生成できる長さは 32 文字までとする
    • これ以上増やすと、連続しないパターンを生成する計算量が爆発する

使用例

Password.new.generate(16)                  # hk84wnIskvCBDITz
Password.new(symbol: true).generate(16)    # EIf{7fpkexis.vvs

実装

class Password
  require 'securerandom'

  CHAR_LIST = 'abcdefghijklmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ123456789'.split('').freeze    # omit o,O,0
  SYMBOL_LIST = '._-=[]{}+#^!?'.split('').freeze
  # SYMBOL_LIST = '"#$%&\'()*+,-./:;<=>?[\]^_`{|}~'.split('').freeze
  SYMBOL_LIST_ESCAPED = SYMBOL_LIST.reduce('') { |r, s| r + '\\' + s }.freeze

  def initialize(symbol: false)
    @symbol = symbol
  end

  def generate(len = 8)
    raise 'too short' if len < 4
    raise 'too long'  if len > 32

    loop do
      result = (0..len - 1).reduce('') { |r, n| r + pick_one(len, n) }
      return result if check(result)
    end
  end

  private

  def pick_one(len, n)
    list = char_list(len, n)

    list[(SecureRandom.random_number(1.0) * list.size).floor]
  end

  def char_list(len, n)
    !@symbol || (n.zero? || n == len - 1) ? CHAR_LIST : CHAR_LIST + SYMBOL_LIST
  end

  def count_symbol(str)
    str.scan(Regexp.new('[%s]' % SYMBOL_LIST_ESCAPED)).size
  end

  def check(str)
    # omit successive chars
    return false if /(.)\1\1/ =~ str

    # confirm the str contains 1..2 symbols
    if @symbol
      return false if count_symbol(str).zero?
      return false if count_symbol(str) > 2
    end

    true
  end
end
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.