AppleScriptで通貨両替計算機を作ってみる

今時1ドルが何円かを知るにはキーワードを「1ドル」にして検索すれば済む話だけど、AppleScriptの理解を深めるために、サンプルプロジェクトとして作ってみた。

順次処理方式

  • 人間が行う手順をそのままコードに置き換える方式。
  • シンプルな処理であれば、コードの流れのままの処理になるので非常に見易い。
display dialog "交換レート" default answer ""
set rate to text returned of result

repeat
  display dialog "金額を入力" default answer "" buttons {"キャンセル", "ドル→円", "円→ドル"}
  set aResult to result
  if button returned of aResult is "ドル→円" then
    display dialog ((text returned of aResult as text) & "ドル → " & (text returned of aResult) * rate as text) & "円"
  else
    display dialog ((text returned of aResult as text) & "円 → " & (text returned of aResult) / rate as text) & "ドル"
  end if
end repeat


  • ドル→円 ボタンの結果

  • 円→ドル ボタンの結果

  • この書き方は、今までの自分のやり方と同じだ。

順次処理方式で4通貨対応にする

  • ドルだけしか両替計算できないのではあまりにもお粗末なので、主要通貨である米ドル、ユーロ、英ポンド、スイスフランの両替計算に対応してみる。
    • 最初に4通貨の交換レートを4回入力してもらう。(交換レートはプロパティなので、次回実行時も保持されている。)
    • 金額を入力して、"外貨→円"ボタン、"円→外貨"ボタンのどちらかを押せば、4通貨の変換金額を一覧表示する。
property kind_list : {"米ドル", "ユーロ", "英ポンド", "スイスフラン"}
property rate_list : {}

if rate_list is {} then
  repeat count of kind_list times
    set rate_list to rate_list & 0
  end repeat
end if

repeat with i from 1 to count of kind_list
  set msg to "交換レート: 1" & item i of kind_list & "は 何円?"
  display dialog msg default answer item i of rate_list
  set item i of rate_list to text returned of result
  log rate_list
end repeat

repeat
  display dialog "金額を入力" default answer "" buttons {"キャンセル", "外貨→円", "円→外貨"}
  set aResult to result
  if button returned of aResult is "外貨→円" then
    set msg to ""
    repeat with i from 1 to count of kind_list
      set msg to msg & (text returned of aResult as text) & item i of kind_list & " → "
      set msg to msg & ((text returned of aResult) * (item i of rate_list) as text) & "円" & return
    end repeat
    display dialog msg
  else
    set msg to ""
    repeat with i from 1 to count of kind_list
      set msg to msg & (text returned of aResult as text) & "円 → "
      set msg to msg & ((text returned of aResult) / (item i of rate_list) as text) & item i of kind_list & return
    end repeat
    display dialog msg
  end if
end repeat



  • ちょっと拡張しただけで、途端にコード量が増えてしまった。
  • しかも、実用的に利用するためには、もう少し改良が必要だ。
    • 金額は3桁区切りにしたい。(カンマで区切る)
    • 長過ぎる小数点以下の桁数も何とかしたい。(2桁に揃える)
    • 入力値のチェックもしておく必要がある。(数値以外の入力はNGにする)
  • そんな機能を実装しようとすると、このようの書き方の延長では途端に複雑怪奇な姿になってくる。

オブジェクト指向で4通貨対応にする

  • そこで同じ機能の両替機を以下のような書き方で実装してみた。(一応オブジェクト指向のつもり)
  • コード量は3倍近くになってしまったが、できるだけ機能別に分けて書いてみた。
    • 両替機オブジェクトを考えてみて、それは円←→ドルのような単機能の両替機なのだが、コピーしていくつも生成することで複数の貨幣に対応する。
    • Exchangerオブジェクトは両替機の金型のようなもので、そこから _Exchanger のコピーを生成する。
    • Exchangerオブジェクトはコピーの生成と同時に、その管理も行う両替機のラックとしての役割もある。
    • ユーザーはラックに組まれた両替機に通貨の種類とレートを設定して利用する。
property kind_list : {"米ドル", "ユーロ", "英ポンド", "スイスフラン"}

init()
input_rate()
repeat
  exchange()
end repeat



--必要なだけ両替機を生成する
on init()
  if my Exchanger's all() is {} then
    repeat with aKind in kind_list
      my Exchanger's new(aKind)
    end repeat
  end if
end init

--変換レートを入力する
on input_rate()
  repeat with aExchanger in my exchanger's all()
    set msg to "交換レート: 1" & aExchanger's currency & "は 何円?"
    display dialog msg default answer aExchanger's rate
    aExchanger's set_rate(text returned of result)
  end repeat
end input_rate

--両替の計算をする
on exchange()
  display dialog "金額を入力してください。" default answer "" buttons {"キャンセル", "外貨→円", "円→外貨"}
  if button returned of result is "外貨→円" then
    display dialog my Exchanger's all_yen_from(text returned of result) as text
  else if button returned of result is "円→外貨" then
    display dialog my Exchanger's all_other_from(text returned of result) as text
  end if
end exchange


--通貨両替機クラス(Exchangerクラスは、_Exchangerインスタンスを生成・管理する能力を持っている)
--このクラスにnewを送信すると、ある外貨の両替に対応した両替機が一つ生成される。
--生成された両替機は、プロパティitem_listに追加され、このクラスでまとめて管理される。
script Exchanger
  property item_list : {}
  
  on new(aCurrency)
    set aItem to _new(aCurrency)
    set item_list to item_list & aItem
    aItem
  end new
  
  on _new(aCurrency)
    script _Exchanger
      property currency : aCurrency
      property rate : 0
      
      on set_currency(amounts)
        set currency to amounts
      end set_currency
      
      on set_rate(theRate)
        set rate to theRate
      end set_rate
      
      --外貨から円に変換する
      on yen_from(other)
        --表示例:"100米ドル → 10000円"
        other & currency & " → " & (other * rate) & "円" & return
      end yen_from
      
      --円から外貨に変換する
      on other_from(yen)
        --表示例:"10000円 → 100米ドル"
        yen & "円" & " → " & (yen / rate) & currency & return
      end other_from
    end script
  end _new
  
  on all()
    item_list
  end all
  
  on all_yen_from(other)
    set msg_list to {}
    repeat with aItem in item_list
      set msg_list to msg_list & aItem's yen_from(other)
    end repeat
    msg_list
  end all_yen_from
  
  on all_other_from(yen)
    set msg_list to {}
    repeat with aItem in item_list
      set msg_list to msg_list & aItem's other_from(yen)
    end repeat
    msg_list
  end all_other_from
end script

オブジェクト指向で実用的な機能も実装する

  • このような書き方にしておくと、機能を拡張する時もあまり散らからずに済みそう。
  • 先程考えていた金額の3桁区切りと小数点の2桁揃えを実装してみた。
    • 簡単そうに見える機能だが、便利な文字列操作や正規表現のライブラリを持たないAppleScriptでは、実装するのはかなり大変だった。
    • 今後のことも考えて、この両替機では使わないが関連する機能をライブラリにまとめて以下のようにしてみた。
    • 結局、すべてをAppleScriptだけでやる気にはなれず、シェルやRubyの機能を借りることになってしまった...。
property kind_list : {"米ドル", "ユーロ", "英ポンド", "スイスフラン"}

init()
input_rate()
repeat
  exchange()
end repeat



 --必要なだけ両替機を生成する
on init()
  if my Exchanger's all() is {} then
    repeat with aKind in kind_list
      my Exchanger's new(aKind)
    end repeat
  end if
end init

 --変換レートを入力する
on input_rate()
  repeat with aExchanger in my exchanger's all()
    set msg to "交換レート: 1" & aExchanger's currency & "は 何円?"
    display dialog msg default answer aExchanger's rate
    aExchanger's set_rate(text returned of result)
  end repeat
end input_rate

 --両替の計算をする
on exchange()
  display dialog "金額を入力してください。" default answer "" buttons {"キャンセル", "外貨→円", "円→外貨"}
  if button returned of result is "外貨→円" then
    display dialog my Exchanger's all_yen_from(text returned of result) as text
  else if button returned of result is "円→外貨" then
    display dialog my Exchanger's all_other_from(text returned of result) as text
  end if
end exchange


 --通貨両替機クラス(Exchangerクラスは、_Exchangerインスタンスを生成・管理する能力を持っている)
 --このクラスにnewを送信すると、ある外貨の両替に対応した両替機が一つ生成される。
 --生成された両替機は、プロパティitem_listに追加され、このクラスでまとめて管理される。
script Exchanger
  property item_list : {}
  
  on new(aCurrency)
    set aItem to _new(aCurrency)
    set item_list to item_list & aItem
    aItem
  end new
  
  on _new(aCurrency)
    script _Exchanger
      property currency : aCurrency
      property rate : 0
      
      on set_currency(amounts)
        set currency to amounts
      end set_currency
      
      on set_rate(theRate)
        set rate to theRate
      end set_rate
      
      --外貨から円に変換する
      on yen_from(other)
        --表示例:"1,000米ドル → 100,000.99円"
        --other & currency & " → " & (other * rate) & "円" & return
        my lib's number_to_currency(other, 0) & my lib's t_left(currency, 6, " ") & " → " & my lib's number_to_currency(other * rate, 2) & "円" & return
      end yen_from
      
      --円から外貨に変換する
      on other_from(yen)
        --表示例:"100,000円 → 1,000.99米ドル"
        --yen & "円" & " → " & (yen / rate) & currency & return
        my lib's number_to_currency(yen, 0) & "円" & " → " & my lib's number_to_currency(yen / rate as text, 2) & currency & return
      end other_from
    end script
  end _new
  
  on all()
    item_list
  end all
  
  on all_yen_from(other)
    set msg_list to {}
    repeat with aItem in item_list
      set msg_list to msg_list & aItem's yen_from(other)
    end repeat
    msg_list
  end all_yen_from
  
  on all_other_from(yen)
    set msg_list to {}
    repeat with aItem in item_list
      set msg_list to msg_list & aItem's other_from(yen)
    end repeat
    msg_list
  end all_other_from
end script



script lib
  --rubyコードを実行して結果を返す
  on do_ruby_script(ruby_code)
    set shell_code to "ruby -e \"puts(" & ruby_code & ")\""
    do shell script shell_code
  end do_ruby_script
  
  --数値を3桁区切りのテキストにする
  on number_with_delimiter(num)
    --注意:バックスラッシュは、バックスラッシュでエスケープすること(\\)。AppleScriptでは特殊な文字として扱われるため。
    --(num.to_s =~ /[-+]?\d{4,}/) ? (num.to_s.reverse.gsub(/\G*1's text 1 thru width
  end t_left
  
  --文字列を右寄せ
  --t_right(文字列, 文字列幅, 埋める文字)
  on t_right(str, width, padding)
    (t_repeat(padding, width) & str)'s text -1 thru -width
  end t_right
  
  --四捨五入する、丸め位置指定可能
  --round_mid(数値, 丸め位置)
  --小数位置は10の指数で指定する
  -- −2: 10^-2...小数第2位まで求める
  -- 3: 10^3...千の位まで求める
  on round_mid(num, place)
    if place ≥ 0 then
      set p to 10 ^ place as integer
    else
      set p to 10 ^ place
    end if
    (round num / p rounding as taught in school) * p
  end round_mid
  
  --切り上げする、丸め位置指定可能
  --round_up(数値, 丸め位置)
  on round_up(num, place)
    if place ≥ 0 then
      set p to 10 ^ place as integer
    else
      set p to 10 ^ place
    end if
    (round num / p rounding up) * p
  end round_up
  
  --切り捨てする、丸め位置指定可能
  --round_down(数値, 丸め位置)
  on round_down(num, place)
    if place ≥ 0 then
      set p to 10 ^ place as integer
    else
      set p to 10 ^ place
    end if
    (round num / p rounding down) * p
  end round_down
end script


  • たったこれだけのことなのだけど、ライブラリもコードの行数としては両替機と同じかそれ以上の規模になってしまった...。
  • このようなプロジェクトにはAppleScriptではなく、もっとライブラリの充実したスクリプト言語を利用すべきだと思った。
  • まだ入力値のチェックはしていない。引き続き実装してみたいのだが、力尽きてこの辺で終了...。
  • 最後に読み直して気付いた。このサンプルの機能なら、ライブラリさえあれば、順次処理方式でもかなりシンプルに書けている。
  • 追加する機能の例が良くない。。さらにこの後、紙幣の種類や手数料計算まで考えると、オブジェクト指向がもっと生きてくるのかもしれない。

解決できない問題

  • ハンドラ呼び出しの時、可変引数に対応したい。
  • そして、デフォルト値を設定しておきたい。

*1:?:\d+\.)?\d{3})(?=\d)/, '\1,').reverse) : num.to_s --(num.to_s =~ /[-+]?\\d{4,}/) ? (num.to_s.reverse.gsub(/\\G((?:\\d+\\.)?\\d{3})(?=\\d)/, '\\1,').reverse) : num.to_s do_ruby_script("('" & num & "' =~ /[-+]?\\d{4,}/) ? ('" & num & "'.reverse.gsub(/\\G((?:\\d+\\.)?\\d{3})(?=\\d)/, '\\1,').reverse) : '" & num & "'") end number_with_delimiter --printfコマンド on printf(format, value) do shell script "printf" & space & format & space & value end printf --金額の書式を整える --number_to_currency(金額, 小数点以下の桁数, 先頭文字, 末尾文字) --lib's number_to_currency(1000.5, 2, "¥", "円") --"¥1,000.50円" --on number_to_currency(num, decimal_place, header, footer) on number_to_currency(num, decimal_place) printf("%" & "." & decimal_place & "f", num) number_with_delimiter(result) --header & result & footer end number_to_currency --文字を繰り返し、連結して返す --t_repeat(文字, 繰り返し回数) on t_repeat(str, aCount) set t to "" repeat aCount times set t to t & str end repeat t end t_repeat --文字列を中央寄せ --t_center(文字列, 文字列幅, 埋める文字) on t_center(str, width, padding) set str_len to str's length set header_len to round_down((width - str_len) / 2, 0) --端数は切り捨て if header_len < 0 then set header_len to 0 end if set footer_len to width - header_len - str_len t_repeat(padding, header_len) & str & t_repeat(padding, footer_len) end t_center --文字列を左寄せ --t_left(文字列, 文字列幅, 埋める文字) on t_left(str, width, padding) (str & t_repeat(padding, width