Tbpgr Blog

Ruby プログラマ tbpgr(てぃーびー) のブログ

Ruby | DSL(Domain Specific Language) 〜 N進数対応表の出力DSLを作ってみる

概要

DSL(Domain Specific Language) 〜 N進数対応表の出力DSLを作ってみる

詳細

DSL(Domain Specific Language)=ドメイン特化言語
Ruby詳しくなくても、対象ドメインに詳しければ動作させることができるような
ミニ言語=ドメイン特化言語を作成する。

代表例としてはRakeのRakefileやBundlerのGemfileなど。
(どちらもRuby自身で構文解析するのでRubyの内部DSL

サンプル仕様

10進数とそれに対応する2進数、8進数、16進数の一覧を表示するためのDSLを作成します。
DSLのファイル名は「Digittabledefine」
DSLのパーサーは「digit_table_define_dsl.rb」
とします。

用意する文法は以下。
また、数値の一覧表示自体は tbpgr_utils gem に実装済みなのでそちらを呼び出します。

文法 設定内容
radix :radix_type 出力対象の基数を数値/String/Symbolのどれかで設定する。設定値は 2,8,16,:all
from :from 出力範囲の指定。開始値を整数で指定
to :to 出力範囲の指定。終了値を整数で指定

from,toのデフォルトはそれぞれ1と10にします。
DSLの記述をしなかったら1,10が設定される。

サンプル

digit_table_define_dsl.rb

require 'tbpgr_utils'

class DigitTableDsl
  attr_reader :radix_type, :from, :to
  module ERROR_MESSAGES
    RADIX_TYPE_ARGUMENT_ERROR = 'invalid Object for radix_type. The Object must implement to_i'
    RADIX_TYPE_INVALID_RANGE = 'invalid Object for radix_type. The Object can use only 2/6/8/16. your input value is %d'
  end
  VALID_RADIX_TYPE_NUMBERS = [2, 4, 8, 16]

  def initialize
    @from = 1
    @to = 10
  end

  def execute(dsl)
    instance_eval dsl
    puts table
  end

  def radix(radix_type = :all)
    check_radix_type(radix_type)
  end

  def from(from = 1)
    @from = Integer(from)
  end

  def to(to = 10)
    @to = Integer(to)
  end

  private
    def check_radix_type(radix_type)
      if ['all', :all].include? radix_type
        @radix_type = :all
        return
      end
      fail ArgumentError, ERROR_MESSAGES::RADIX_TYPE_ARGUMENT_ERROR unless radix_type.respond_to? :to_i
      @radix_type = Integer(radix_type)
      fail ArgumentError, format(ERROR_MESSAGES::RADIX_TYPE_INVALID_RANGE, @radix_type) unless VALID_RADIX_TYPE_NUMBERS.include? @radix_type
    end

    def check_from_or_to(from_or_to)
      fail ArgumentError, ERROR_MESSAGES::RADIX_TYPE_ARGUMENT_ERROR unless VALID_RADIX_TYPE_NUMBERS.ciradix_type.respond_to? :to_i
    end

    def table
      case @radix_type
      when 2
        Fixnum.to_binary_table @from, @to
      when 8
        Fixnum.to_oct_table @from, @to
      when 16
        Fixnum.to_hex_table @from, @to
      when :all
        Fixnum.to_digit_table @from, @to
      end
    end
end

dsl = File.read('./Digittabledefine')
DigitTableDsl.new.execute dsl

実行例1

全基数出力。from/toはデフォルトの設定(1-10)を利用。

Digittabledefine

radix :all

出力

|10digit|  2digit|8digit|16digit|
|      1|00000001|    01|   0001|
|      2|00000010|    02|   0002|
|      3|00000011|    03|   0003|
|      4|00000100|    04|   0004|
|      5|00000101|    05|   0005|
|      6|00000110|    06|   0006|
|      7|00000111|    07|   0007|
|      8|00001000|    10|   0008|
|      9|00001001|    11|   0009|
|     10|00001010|    12|   000a|

実行例2

2進数のみ。1から16。
Digittabledefine

radix 2
from 1
to 16

出力

|10digit|  2digit|
|      1|00000001|
|      2|00000010|
|      3|00000011|
|      4|00000100|
|      5|00000101|
|      6|00000110|
|      7|00000111|
|      8|00001000|
|      9|00001001|
|     10|00001010|
|     11|00001011|
|     12|00001100|
|     13|00001101|
|     14|00001110|
|     15|00001111|
|     16|00010000|

radix, from, to のすべては Kernel#Integer で整数に変換しています。
例えば整数型のInteger(Fixnum/Bignum)以外にも数値文字列(各基数表記に対応)、浮動小数(Float)に対応していて、
それぞれ整数に変換されて設定されます。

これにより、Integerのみを許容する場合に比べて様々な指定方法が利用できて柔軟なIFになります。
これは書籍 Confident Ruby から学んだ知識とテクニック。

例えば以下のような Digittabledefine でも戦術と同じ結果が出力されます。

Digittabledefine (10進文字列、2進文字列、8進文字列)で指定

radix '2'
from '0b01'
to '020'

Digittabledefine (Fixnum、Float、16進文字列)で指定

radix 2
from 1.9
to '0x10'

実行例3

8進数のみ。8から32。

Digittabledefine

radix 8
from 8
to 32

出力

$ ruby digit_table_dsl.rb
|10digit|8digit|
|      8|    10|
|      9|    11|
|     10|    12|
|     11|    13|
|     12|    14|
|     13|    15|
|     14|    16|
|     15|    17|
|     16|    20|
|     17|    21|
|     18|    22|
|     19|    23|
|     20|    24|
|     21|    25|
|     22|    26|
|     23|    27|
|     24|    30|
|     25|    31|
|     26|    32|
|     27|    33|
|     28|    34|
|     29|    35|
|     30|    36|
|     31|    37|
|     32|    40|

実行例4

16進数のみ。16から32。

Digittabledefine

radix 16
from 16
to 32

出力

|10digit|16digit|
|     16|   0010|
|     17|   0011|
|     18|   0012|
|     19|   0013|
|     20|   0014|
|     21|   0015|
|     22|   0016|
|     23|   0017|
|     24|   0018|
|     25|   0019|
|     26|   001a|
|     27|   001b|
|     28|   001c|
|     29|   001d|
|     30|   001e|
|     31|   001f|
|     32|   0020|

実行例5

不整値いろいろ。

対応していない奇数の場合
Digittabledefine

radix 3
from 16
to 32

出力

digit_table_dsl.rb:17:in `instance_eval': invalid Object for radix_type. The Object can use only 2/6/8/16. your input value is 3 (ArgumentError)
        from digit_table_dsl.rb:22:in `radix'
        from (eval):1:in `execute'
        from digit_table_dsl.rb:17:in `instance_eval'
        from digit_table_dsl.rb:17:in `execute'
        from digit_table_dsl.rb:63:in `<main>'

Kernel#Integerに対応していない値を指定した場合
Digittabledefine

radix 2
from 'invlalid'
to 32

出力

digit_table_dsl.rb:17:in `instance_eval': invalid value for Integer(): "invlalid" (ArgumentError)
        from digit_table_dsl.rb:26:in `from'
        from (eval):2:in `execute'
        from digit_table_dsl.rb:17:in `instance_eval'
        from digit_table_dsl.rb:17:in `execute'
        from digit_table_dsl.rb:63:in `<main>'