ルビー3.0の出現-日04 -パスポート処理


ルビー3.0はちょうどリリースされたので、我々は実験に時間がかかると、すべての楽しい新機能がそこにあるかを参照してください年の陽気な時間です.
この一連の投稿では、Ruby 2.7と3.0の機能をいくつか紹介しますsolve Advent of Code problems . ソリューション自体は、機能の新しい使用を示すために最も効率的であることを意味していません.
これらのポストの各々が次第に長く、より関与するように、私は個々の日までにこれらを壊しています.
そう言ってしまおうじゃないか


Day 04 - パート01 -パスポート処理
これは、最後のものよりかなり簡単でした.いくつかの矛盾した形式のデータをkey:value そして、妥当性検査はすべての必要なキーが存在することを確認することです.
解決策を見てみましょう.
require 'any'

VALID_KEYS = %i(byr iyr eyr hgt hcl ecl pid cid)
VALID_KEYS_MAP = VALID_KEYS.to_h { |k| [k, Any] }
VALID_NORTH_POLE_MAP = VALID_KEYS_MAP.except(:cid)

def valid_passports(passports)
  passports.filter_map do |passport|
    parsed_passport = passport
      .split
      .to_h { _1.split(':') }
      .transform_keys(&:to_sym)

    parsed_passport if parsed_passport in VALID_NORTH_POLE_MAP
  end
end

def valid_passport_count(...) = valid_passports(...).size

File.read(ARGV[0]).split(/\n\n/).then { puts valid_passport_count(_1) }

何か?any 何かについて比較したときに、本当に応える私自身の作りの宝石です、しかし、特に=== . パターンマッチング用途=== これは、次のように動作します.
{ a: 1 } in a: Any
なぜ?何故なら、私たちは常に真実のものが欲しいからです.実験から、これは仕事にダイナミックであるパターンを得る唯一のきれいな方法です.

有効なキーとマッピング
まず最初にしたいことは、すべての有効なパスポートのリストを確認することです.
VALID_KEYS = %i(byr iyr eyr hgt hcl ecl pid cid)
...そして、その後、我々Any キーの任意のセットに対するパターンマッチングについて何か楽しいことを示すトリック
VALID_KEYS_MAP = VALID_KEYS.to_h { |k| [k, Any] }
こうすることで、次のようなことができます.
some_hash in VALID_KEYS_MAP
...または、この検証の場合、私たちは:cid , そしてRuby 3はRailsランドから新しい機能を提供します.except :
VALID_NORTH_POLE_MAP = VALID_KEYS_MAP.except(:cid)
どのような場合は、1つまたは1つのリストを除いてすべてのキーを考えるかもしれません.
今では私たちをもたらすvalid_passports .
to_h キーペアto_h Ruby 2.7の関数を使用します.map.to_h :
def valid_passports(passports)
  passports.filter_map do |passport|
    parsed_passport = passport
      .split
      .to_h { _1.split(':') }
    # ...
それで、そのファイルのすべてのそれらの任意のkeypair?split Whitespaceによって行きます.そして、それがフォーマット視差を扱うことができることを意味します、そして、我々はまっすぐにそれを供給することができますto_h 我々が欲しいハッシュにそれを作るために.
パターンマッチングの理由で再びシンボルキーに変換することに気づくかもしれません.
.transform_keys(&:to_sym)

パターンに織られる
では、次のようになります.
parsed_passport if parsed_passport in VALID_NORTH_POLE_MAP
...はparsed_passport すべての有効な北極のキーが存在します.与えられたキーの比較を行うことができましたが、Ruby 3.0デモは今のところ優先順位があります.
なぜparsed_passport if ? 私たちはfilter_map これは、私たちが有効なパスポートを保つだけでなく、新たに逆シリアル化(ハッシュ化)形式でそれらを保つことができます.
これらの値を直接必要としない間、デバッグのために見たり、必要に応じて後で変更する場合には、非常に有用です.

カウント
それから、私たちは、カウントを得るために上記の機能をラップするために、私たちの古い無限の機能トリックにダウンします
def valid_passport_count(...) = valid_passports(...).size

異なるものに分割する
あなたは私が使用しなかったことに気づくかもしれないreadlines こちら
File.read(ARGV[0]).split(/\n\n/).then { puts valid_passport_count(_1) }
これは、各レコードに空白行があるか、または\n\n , それらの間に.改行に分割すれば、混乱したレコードの断片が得られるだろう.これで、我々は我々の機能にまっすぐに記録を供給することができます、そして、離れて、我々は行きます.
...そして、それで、我々は日4のパート1のために我々の解決をします.

日04 -パート02 -パスポートの検証
Part 2はかなりの数の検証を加え、Rubyのすべての種類の興味深い機能を試す機会を与えてくれます.解決策を見て始めましょう、これは旅行です.
require 'any'

str_int_within = -> range { -> v { range.cover? v.to_i } }

HEIGHT_REGEX          = /(?<n>\d+) ?(?<units>cm|in)/
HAIR_COLOR_REGEX      = /^\#[0-9a-z]{6}$/
PASSPORT_ID_REGEX     = /^[0-9]{9}$/
EYE_COLOR_REGEX       = Regexp.union(*%w(amb blu brn gry grn hzl oth))
VALID_BIRTH_YEAR      = str_int_within[1920..2002]
VALID_ISSUED_YEAR     = str_int_within[2010..2020]
VALID_EXPIRATION_YEAR = str_int_within[2020..2030]
VALID_CM_HEIGHT       = str_int_within[150..193]
VALID_IN_HEIGHT       = str_int_within[59..76]

VALID_HEIGHT =
  -> { HEIGHT_REGEX.match(_1) } >>
  -> { _1&.named_captures } >>
  -> { _1&.transform_keys(&:to_sym) } >>
  -> {
    case _1
    in units: 'cm', n: VALID_CM_HEIGHT
      Any
    in units: 'in', n: VALID_IN_HEIGHT
      Any
    else
      nil
    end
  }

def valid_passports(passports)
  passports.filter_map do |passport|
    parsed_passport =
      passport
            .split
            .to_h { _1.split(':') }
            .transform_keys(&:to_sym)

    parsed_passport if parsed_passport in {
      byr: VALID_BIRTH_YEAR,
      iyr: VALID_ISSUED_YEAR,
      eyr: VALID_EXPIRATION_YEAR,
      hgt: VALID_HEIGHT,
      hcl: HAIR_COLOR_REGEX,
      ecl: EYE_COLOR_REGEX,
      pid: PASSPORT_ID_REGEX
    }
  end
end

def valid_passport_count(...) = valid_passports(...).size

File.read(ARGV[0]).split(/\n\n/).then { puts valid_passport_count(_1) }
今、ここで多くの楽しみ、多くの楽しいことを探索するので、それを取得しましょう.

すぐ閉鎖
この行はクロージャと呼ばれます.
str_int_within = -> range { -> v { range.covers? v.to_i } }
関数がなぜ関数を返すのか?とても便利だから!ちょっと簡単なものから始めましょう.
adds = -> a { -> b { a + b } }
コールすると、呼び出したものを記憶する関数を返します.a :
adds_3 = adds[3]
adds_3[3]
# => 6
他の関数に渡すこともできます.
[1, 2, 3].map(&adds[3])
# => [4, 5, 6]
だからstr_int_within , 文字列として表される数が数年の範囲内か数であるかを調べることができる関数を求めます.
str_int_within = -> range { -> v { range.covers? v.to_i } }
最初の定数を見てみましょう.
VALID_BIRTH_YEAR = str_int_within[1920..2002]
以下のようにテストできます:
VALID_BIRTH_YEAR['1800']
# => false

VALID_BIRTH_YEAR['2000']
# => true

array_of_birth_years.select(&VALID_BIRTH_YEAR)
したがって、この概念は、実際に柔軟性があり、さらにパターンマッチングで使用することができます.クロージャについてもっと知りたい場合はgive this article a read .
この設定を使用して検証の定数を設定します.
VALID_BIRTH_YEAR      = str_int_within[1920..2002]
VALID_ISSUED_YEAR     = str_int_within[2010..2020]
VALID_EXPIRATION_YEAR = str_int_within[2020..2030]
VALID_CM_HEIGHT       = str_int_within[150..193]
VALID_IN_HEIGHT       = str_int_within[59..76]

いくつかのクイックregex
最初の検証定数のセットを見てみましょう.
HEIGHT_REGEX          = /(?<n>\d+) ?(?<units>cm|in)/
HAIR_COLOR_REGEX      = /^\#[0-9a-z]{6}$/
PASSPORT_ID_REGEX     = /^[0-9]{9}$/
EYE_COLOR_REGEX       = Regexp.union(*%w(amb blu brn gry grn hzl oth))
なぜ定数?これらの検証名を与えて、何をするかを説明できます.
ここの面白いものはRegexp.union これにより、複数の正規表現や文字列を結合することができます.我々が使用することを許可include? , でもArray に応答しません=== パターンマッチングのための非理想化regexは以下のようにします.
/abc/ === 'abc'
# => true

case something
when /abc/ then true
else false
end
それでcase / when and case / in 値と一致するように動作します.=== . 気の利いたもの、そして恥Array and Hash 実装しないでください.

楽しい構成
今、この1つは確かに少しoverkillよりも行くが、それにもかかわらず、概念を実証するために楽しいです:
VALID_HEIGHT =
  -> { HEIGHT_REGEX.match(_1) } >>
  -> { _1&.named_captures } >>
  -> { _1&.transform_keys(&:to_sym) } >>
  -> {
    case _1
    in units: 'cm', n: VALID_CM_HEIGHT
      Any
    in units: 'in', n: VALID_IN_HEIGHT
      Any
    else
      nil
    end
  }
最初の行はHEIGHT_REGEX 上から、それが有効な高さであるかどうかチェックしてくださいMatchDataunit そして、何があるか.
関数を作成した後のシンボル、または一緒に置きます.それは最初に通過します、そして、それから第1の出力は第2を通して行きます、そして、同様に.
番目のラインは、最初の可能性がありますリターンに対して警戒しているnil 孤独な演算子を使用することによって&. ) 返り値nil 呼び出すならばnil , またはいくつかの有効な場合は、実際の関数を呼び出してMatchData 仕事をする.
三行目のパターンマッチングのキーを象徴する私たちのトリックに戻ります.
第4は、それが完全に少しおもしろくなるところですcase / in パターンマッチ:
case _1
in units: 'cm', n: VALID_CM_HEIGHT
  Any
in units: 'in', n: VALID_IN_HEIGHT
  Any
else
  nil
end
この点で_1 当社のキャプチャグループ、またはそれはnil . もしそうならnil または上記の条件にマッチしませんnil 無効な高さを表す.有効なケースはもっと面白いものだ.それはunits and n 我々の試合から、使用して値を比較します=== . この場合、これらの正規表現は上からです.
我々がaに供給するならば、一緒にこれを持ってくることString 高さを含んでいるので、それが有効であるかどうかについて、我々が一致することができて、言うことができる形式であるまで、シーケンスのすべてのそれらの機能を通過します.

一緒にもたらす
今、この解決策は、パターンマッチの明確なラインに私たちをもたらします.何も他の何も変化していないが、これは?これは私に若干の喜びをもたらします.
parsed_passport if parsed_passport in {
  byr: VALID_BIRTH_YEAR,
  iyr: VALID_ISSUED_YEAR,
  eyr: VALID_EXPIRATION_YEAR,
  hgt: VALID_HEIGHT,
  hcl: HAIR_COLOR_REGEX,
  ecl: EYE_COLOR_REGEX,
  pid: PASSPORT_ID_REGEX
}
このような形式で検証を表現できます.これは、JSONや他のデータを検証する可能性の全世界を開き、私は私の将来のためのいくつかのアイデアがあります.
インライン式を使用できないことに注意してください.ピンとparensで可能なバグレポートがあります.^(expr) , しかし、速度に関する若干の懸念は、それとともに来ます.
それはパート2を包みます、そして、それは旅行実験、そして、私がとても楽しんだ何かでした.

日04ラップ
それが4日目に包まれることについて、我々はこれらの問題の各々を通して働き続けて、次の数日と数週にわたって彼らの解決と方法論を調査し続けるでしょう.
場合は、元のソリューションのすべてをチェックアウトするthe Github repo 完全にコメントソリューションです.