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