3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

学校の課題になりそうな文字列計算機をワンライナーで作ってみた

Last updated at Posted at 2023-11-03

TL;DR;

"1+2*3-4"という文字列を先頭から順に計算((1+2)*3-4)して5を出力するコードを1行で書いてみた。

Regex.Matches("1+2*3-4", @"(^|[+\-*/]) *(\d+) *").Cast<Match>().Select(m => new { Op = m.Groups[1].Value, Val = m.Groups[2].Value }).Aggregate((r, c) => new { Op = c.Op, val = new DataTable().Compute(r.Val + c.Op + c.Val, "").ToString() }).Val;

ネタ

C#の課題で下記の問題があったと仮定する。

1+2*3-4のような文字列を受け取ると先頭から順に自然数の加減乗除を行い、計算結果を表示するプログラムを作りなさい。
なお演算子の優先順位は考慮せず括弧は使用しないものとする。

演算子の優先順位などを考慮して文字列を動的に計算するならばSystem.Data.DataTable.Computeで実現できる。

var s = "1+2*3-4";
var result = new System.Data.DataTable().Compute(s, "");
Console.WriteLine(result);  // 3

しかし先頭から順に計算すると(1+2)*3-4の順に計算して5を導出しなければならない。
出題者はfor文やif文を使ってループ、条件分岐、変数の基本を学ばせたいのであろう。
初学者にとってはなかなか難しい問題なので、プログラムが苦手な人は頭がこんがらかってしまうかもしれない。

そんな人に意地悪な先輩が「ふん、そんなに苦手なら**ifintforも使わなければいい**だろう」と言うかもしれない。
その挑発にあなたは「できらぁっ!」と返してしまったとしよう。

安心してほしい。
それだけならcase文とlong型とforeach文を使えば朝飯前だ。
だがきっと意地悪な先輩はニヤリと笑ってこう続けるのだ。

「ならばやって見せろ。ifintforcase三項演算子longfloatdoubledecimalshortushortuintulongforeachwhiledo-whilegoto再帰処理NuGetパッケージも使わずにな!」
と、後付けの方が長いセリフを。

安心してほしい。
できらぁ!

サンプルコード

using System;
using System.Data;
using System.Linq;
using System.Text.RegularExpressions;
 
namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var s = "1+2*3-4";
            // ワンライナー
            var result = Regex.Matches(s, @"(^|[+\-*/]) *(\d+) *").Cast<Match>().Select(m => new { Op = m.Groups[1].Value, Val = m.Groups[2].Value }).Aggregate((r, c) => new { Op = c.Op, val = new DataTable().Compute(r.Val + c.Op + c.Val, "").ToString() }).Val;
            Console.WriteLine(result);
        }
    }
}

このコードではforifintも使っていない。
DataTableに加えて正規表現とAggregateと匿名型があればワンライナーで実現できる。

さあ、このコードを提出して先輩や他の生徒に差をつけよう!
などという甘言に乗ると普通に単位を落とすので注意すること。

解説のようなもの

ざっくり。

Regex.Matches(s, @"(^|[+\-*/]) *(\d+) *")

(行頭または[四則演算子])の後に連続する数字にマッチする正規表現である。(数字の前後には0個以上のスペースを含むことができる)

例えば12+ 3-567 であれば、12+ 3-567 がそれぞれマッチ対象となる。
Regex.Matchesの戻り値MatchCollectionには、上記の12+ 3-567 がコレクションとして格納される。

.Cast<Match>().Select(m => new { Op = m.Groups[1].Value, Val = m.Groups[2].Value })

.Cast<Match>()は、MatchCollectionをLinqで使えるようにするおまじない。

new { Op = "hoge", Val = "fuga" }匿名型の宣言である。
OpVal変数を持つクラス名のないオブジェクトを作ることができる。

正規表現の()の中身はマッチごとにグループとして取り出せる。
前述の例ならば、12+ 3-567 はそれぞれ["", "12"]["+", "3"]["-", "567"]がグループ配列になると考えてよい。
(インデックス0はマッチした文字列全体が入るので、インデックスは1から始まる)

.Aggregate((r, c) =>
    new {
        Op = c.Op,
        val = new DataTable().Compute(r.Val + c.Op + c.Val, "").ToString() // 計算結果の戻り値(Object型)を文字列型に変換
        }).Val; // 計算結果の値(Val)を取得する

LinqのAggregateは配列を先頭から順に取り出して自由に集計できる式だ。
例えばnew int[] { 1, 2, 3 }.Aggregate((r, c) => c%2==0 ? r+c : r-c)を考えてみよう。
Aggregateは最初にr=1, c=2を代入して計算を行い、その結果の3を次のrに代入する。そして次の値の3をcに代入してまた計算を行い、すべての計算結果の0を返す。

その計算を既出のSystem.Data.DataTable.Computeに肩代わりしてもらうことで、先頭から順に計算した結果を取得できるのだ。

正直こんなざっくり解説を聞いてもさっぱり分からない方が多数派だと思うが、ネタなのでこの辺で。
あとはググったりコーディングして自分の目で確かめてくれ!

言い訳

ちなみに私自身が、このコードを作ったはいいけど使いどころがなくてネタとして供養したかったのがこの記事のきっかけである。
中級者以上の方にご笑納いただいたり、正規表現やLinqや匿名クラスが分からない方の学習材料になれば望外の喜びである。

3
3
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?