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文を使ってループ、条件分岐、変数の基本を学ばせたいのであろう。
初学者にとってはなかなか難しい問題なので、プログラムが苦手な人は頭がこんがらかってしまうかもしれない。
そんな人に意地悪な先輩が「ふん、そんなに苦手なら**if
もint
もfor
も使わなければいい**だろう」と言うかもしれない。
その挑発にあなたは「できらぁっ!」と返してしまったとしよう。
安心してほしい。
それだけならcase
文とlong
型とforeach
文を使えば朝飯前だ。
だがきっと意地悪な先輩はニヤリと笑ってこう続けるのだ。
「ならばやって見せろ。if
もint
もfor
もcase
も三項演算子
もlong
もfloat
もdouble
もdecimal
もshort
もushort
もuint
もulong
もforeach
もwhile
もdo-while
もgoto
も再帰処理
も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);
}
}
}
このコードではfor
もif
もint
も使っていない。
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" }
は匿名型の宣言である。
Op
とVal
変数を持つクラス名のないオブジェクトを作ることができる。
正規表現の()
の中身はマッチごとにグループとして取り出せる。
前述の例ならば、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や匿名クラスが分からない方の学習材料になれば望外の喜びである。