見出し画像

#9 「逆ポーランド記法電卓"RPN"を作る」_Rustを分かりたい

また別の言語に惹かれています。どちらかというと備忘録。

注意

この記事(今シリーズ)は初心者がRustをかじりながら、備忘録のような形で投稿していく予定です。
そのため、今シリーズ全体を通して信憑性は非常に低いです。
また専門の方などから見れば、無茶苦茶なこと、おかしなことをしているかもしれませんがご容赦ください。


前回

逆ポーランド記法(RPN)電卓とは

逆ポーランド記法(以下RPN)とは、式の記述法には主に中置記法、前置記法、後置記法と3つあります。これら以外にもあるかも?ちなみにRPNはReverse Polish Notation。

中置記法とは普段数学で使用する「1 + 2」のように被演算子(数字、オペランド)の間に演算子を記述する方法です。

それと違い前置記法は「+ 1 2」のように演算子を前に持っていきます。後置記法(RPN)では「1 2 +」のように演算子を後ろに記述します。

逆ポーランド記法では後で使われる演算子ほど、右に位置することになる(ポーランド記法では逆になり、左に位置する演算子ほど後で使われる)。

逆ポーランド記法_Wikipedia

値を配列等に貯めた後に、演算法を決定できるため楽…?

まず計算法

実は特に調べずにコードを書き始めたのですが、上手くいかないので何かを参照することに…

ただ、rustで絞って検索すると答え見る気がして練習にならないので、qiitaのc(だった気がする)言語での参考例のコードでない部分、ロジックの説明だけを見るようにしました。(cとは違うんでそこまで意味あるとは言えないかも知れませんが…)

以下参考。

となります。
読み方(解き方)としては、({}はスタック)
0, 左から順に読んでいく
1, 3をスタックに入れる{3}
2, 5をスタックに入れる{3 5}
3, +になるのでスタックから3と5を取り出し、演算子にあった計算(今回は加算)を行い計算結果をスタックに入れる{8}
4, 1をスタックにいれる{8 1}
5, 1をスタックにいれる{8 1 1}
6, +になるのでスタックから1と1を取り出し、演算子にあった計算(今回は加算)を行い計算結果をスタックに入れる{8 2}
7, /になるのでスタックから8と2を取り出し、演算子にあった計算(今回は除算)を行い計算結果をスタックに入れる{4}
答えは4

といった読み方(解き方)をしていきます。

逆ポーランド記法を理解するためにC言語で実装してみた_Qiita

一行ずつ

*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); //結果の挿入
    }
}

1 2 + の場合

1 2 + 3 +の場合

1 2 + 3 4 + *の場合

次回


この記事が気に入ったらサポートをしてみませんか?