#9 「逆ポーランド記法電卓"RPN"を作る」_Rustを分かりたい
また別の言語に惹かれています。どちらかというと備忘録。
注意
この記事(今シリーズ)は初心者がRustをかじりながら、備忘録のような形で投稿していく予定です。
そのため、今シリーズ全体を通して信憑性は非常に低いです。
また専門の方などから見れば、無茶苦茶なこと、おかしなことをしているかもしれませんがご容赦ください。
前回
逆ポーランド記法(RPN)電卓とは
逆ポーランド記法(以下RPN)とは、式の記述法には主に中置記法、前置記法、後置記法と3つあります。これら以外にもあるかも?ちなみにRPNはReverse Polish Notation。
中置記法とは普段数学で使用する「1 + 2」のように被演算子(数字、オペランド)の間に演算子を記述する方法です。
それと違い前置記法は「+ 1 2」のように演算子を前に持っていきます。後置記法(RPN)では「1 2 +」のように演算子を後ろに記述します。
値を配列等に貯めた後に、演算法を決定できるため楽…?
まず計算法
実は特に調べずにコードを書き始めたのですが、上手くいかないので何かを参照することに…
ただ、rustで絞って検索すると答え見る気がして練習にならないので、qiitaのc(だった気がする)言語での参考例のコードでない部分、ロジックの説明だけを見るようにしました。(cとは違うんでそこまで意味あるとは言えないかも知れませんが…)
以下参考。
一行ずつ
*ver.1.1.3は指数演算ができないなど割とバグがあります。
一行もしくは1操作ずつ説明していこうと思います。ソース全文は↑。今回は割と処理を関数に分けているので、どちらかというと関数の説明が主になるかも知れません。
main関数
コードでは一番下にいますが、分かりにくいのでここで。関数の軽い説明と全体の動作をまず説明。
loop
プログラム全体のループ。
値の入力装置
println!("式を入力してください。\n例: 1 + 2 → 1 2 +\n値や演算子同士は半角スペースで区切ってください。");
let input_formula = get_input();
入力の要求と説明。
余計な文字列の確認
abcとか余計な文字列が含まれないことを確認。含まれていた場合continueでループの最初までスキップ。
if check_syntax(&input_formula) == false {
println!("計算不可能な文字が含まれています。もう1度入力してください");
continue;
}
let delimited_input_fomula = delimit(&input_formula);
半角スペースで入力を分割して、各演算子、被演算子に分けます。例えば、「1 2 +」を入力されたらdelimit関数で「1」「2」「+」を各要素としたベクタを「delimited_input_fomula」に束縛。
let delimited_input_fomula = delimit(&input_formula);
let result = stack_manage(delimited_input_fomula);
先ほどのベクタを計算橋渡しであるstack_manage関数に渡し、計算結果をresult変数に束縛。
let result = stack_manage(delimited_input_fomula);
もう一度計算?
結果の表示ともう一度計算するかの要求。
println!("{}\nもう一度計算しますか?(y/n)", result);
if get_input() == "n".to_string() {
break;
}
get_input関数
コンソールで入力を受け取りString型で戻す関数。Pythonのinput()のような使い方をしています。
fn get_input() -> String { //String型で入力を返す
let mut word = String::new();
std::io::stdin()
.read_line(&mut word)
.expect("Failed to read line");
word.trim().to_string()
}
String型の空の変数「word」を作成、std::io::stdin()で入力をwordに入れます。.expect()は失敗した際の戻り値(?)です。
.trim()
これは先頭と末尾のスペースを削除してます。
.to_string()
String回でもおそらく説明していますが、String型に変換する。
word.trim().to_string()
;を付けてないため、関数の戻り値になる。
check_syntax
シンタックスという表記が妥当かはわかりませんが、abcとかの余計な文字列が含まれないことを確認するもの。
fn check_syntax(checked_string: &String) -> bool { //入力に演算不可能な文字があった場合false
let re = Regex::new("[^+\\-*/%1234567890 ]").unwrap();
return if re.is_match(&checked_string) {false} else {true};
}
use regex::Regexを使用して正規表現で検索します。RegexはCargo.tomiファイルに以下を追記。
[dependencies]
regex = "1.10.4"
[^+\\-*/%1234567890 ]
は[^]で中身を反転するそう?演算子と数字以外の値を含んだ場合を検索。
return if re.is_match(&checked_string) {false} else {true};
含んでいたらfalse、含んでいないならtrueを返す。
delimit
半角スペースで分割。.split_whitespace().collect()したものを返す。
fn delimit(input: &String) -> Vec<&str>{ //文字列を空白で区切りベクタにして返す
input.split_whitespace().collect()
}
is_numeric関数
受け取った&strがf64に変換可能ならtrue、ダメならfalseを返す。
fn is_numeric(input: &str) -> bool { //入力が数値ならtrue, 演算子ならfalse
match input.parse::<f64>() {
Ok(_) => true,
Err(_) => false,
}
}
calculation関数
被演算子2個に演算子1個で各演算をする。
fn calculation(operand_1: f64, operand_2: f64, operator: &str) -> f64 { //演算
match operator {
"+" => operand_1 + operand_2,
"-" => operand_1 - operand_2,
"*" => operand_1 * operand_2,
"/" => operand_1 / operand_2,
"%" => operand_1 % operand_2,
"**" => power(operand_1, operand_2),
_ => 0.0,
}
}
power関数
標準ライブラリだかにあるそうですが、一応練習のため指数演算を行う関数。
fn power(operand_1: f64, operand_2: f64) -> f64 { //指数演算
let mut power_result = operand_1;
for _ in 0..operand_2 as i64 - 1 {
power_result *= operand_1
}
power_result
}
forでiを使わない場合_にするといいらしい。
stack_manage関数
変数の設定
let mut stack = Vec::<f64>::new();
let mut result = 0.0;
スタック分全て計算
関数の引数はベクタのためforで全てを参照できる。
まずif is_numeric(i)でその要素が被演算子(数値)か演算子かを判断する。被演算子の場合スタックに追加する。演算子の場合、スタックの後ろから2数(末尾と末尾から1個手前)を計算します。
末尾はlen()で長さを知りそれの一個手前len() - 1と-2を計算します。
.removeで今使った被演算子を削除します。次に先ほどのresultを次の計算のためにスタックに追加します。
for i in delimited_input {
if is_numeric(i) == true { //オペランドの場合
stack.push(i.parse::<f64>().unwrap_or(0.0));
} else { //演算子の場合
result = calculation(stack[stack.len() - 2], stack[stack.len() - 1], i);
for _ in 0..2 {
stack.remove(stack.len() - 1);
}
stack.push(result); //結果の挿入
}
}
戻り値
;を付けずに結果を戻す。
result
実行の例
流れを図解?しようかと思います。処理順の番号を振っているので実際のコードとは違います。例: *1。
for i in delimited_input {
if is_numeric(i) == true { //オペランドの場合
*1 stack.push(i.parse::<f64>().unwrap_or(0.0));
} else { //演算子の場合
result = calculation(stack[stack.len() - 2], stack[stack.len() - 1], i);
for _ in 0..2 {
*2 stack.remove(stack.len() - 1);
}
*3 stack.push(result); //結果の挿入
}
}