比較関数を渡せる GroupBy メソッドを作る

比較関数を渡せる GroupBy メソッドを作ってみました。

using System;
using System.Collections.Generic;
using System.Linq;

class Program {
    static IEnumerable<T[]> GroupBy<T>(IEnumerable<T> xs, Func<T, T, bool> match) {
        var ls = new List<T>();
        foreach (var x in xs) {
            if (ls.Count == 0 || match(ls[ls.Count-1], x)) {
                ls.Add(x);
            }
            else {
                yield return ls.ToArray();
                ls.Clear();
                ls.Add(x);
            }
        }
        if (ls.Count > 0) {
            yield return ls.ToArray();
        }
    }

    static void Display<T>(IEnumerable<T[]> xxs) {
        var xs = xxs.Select(e => string.Format("[{0}]", string.Join(", ", e)));
        var s = string.Format("[{0}]", string.Join(", ", xs));
        Console.WriteLine(s);
    }

    static void Main() {
        Display(GroupBy(new[] { 1, 2, 1, 2, 5, 1 }, (a, b) => a < b));
        // => [[1, 2], [1, 2, 5], 1]

        Display(GroupBy(new[] { 1, 1, 1, 2, 2, 5 }, (a, b) => a == b));
        // => [[1, 1, 1], [2, 2], [5]

        Display(GroupBy(new[] { 1, 1, 1, 2, 2, 5 }, (a, b) => a != b));
        // => [[1], [1], [1, 2], [2, 5]]

        Display(GroupBy("Mississippi", (a, b) => a == b));
        // => [[M], [i], [s, s], [i], [s, s], [i], [p, p], [i]]
    }
}

実行結果です。

[[1, 2], [1, 2, 5], [1]]
[[1, 1, 1], [2, 2], [5]]
[[1], [1], [1, 2], [2, 5]]
[[M], [i], [s, s], [i], [s, s], [i], [p, p], [i]]

リンク

1 次元配列の回転

1 次元配列を回転するサンプルです。

using System;
using System.Collections.Generic;
using System.Linq;

class Program {
    static T[] Rotate<T>(T[] xs, int n) {
        var ret = new T[xs.Length];
        if (ret.Length == 0) return ret;

        int m = n % ret.Length;
        for (int i = 0; i < ret.Length; i++) {
            ret[i] = xs[m++];
            if (m == ret.Length) m = 0;
        }
        return ret;
    }

    static void Main() {
        var xs = Enumerable.Range(1, 5).ToArray();
        for (int i = 0; i < 7; i++) {
            var ys = Rotate(xs, i);
            Console.WriteLine(string.Join(" ", ys));
        }
    }
}

実行結果です。

1 2 3 4 5
2 3 4 5 1
3 4 5 1 2
4 5 1 2 3
5 1 2 3 4
1 2 3 4 5
2 3 4 5 1

インスタンスが null の場合でも拡張メソッドの呼び出しは可能

インスタンスが null の場合にメソッド呼び出しを行うと、System.NullReferenceException 例外が生成されますが、拡張メソッドの場合は、インスタンスが null でも呼び出しが可能です。

using System;

public class Foo {
    public void Hello() {
        Console.WriteLine("Hello!!");
    }
}

public static class Extension {
    public static void Goodbye(this Foo foo) {
        Console.WriteLine("Goodbye!!");
    }
}

class Program {
    static void Main() {
        Foo foo = new Foo();
        foo.Hello();       // Hello
        foo.Goodbye();     // Goodbye!!

        Foo foo2 = null;
        // foo2.Hello();   // System.NullReferenceException
        foo2.Goodbye();    // Goodbye!!
    }
}

実行結果です。

Hello!!
Goodbye!!
Goodbye!!

Partition 関数

シーケンスの要素を、述語関数を満たす要素と満たさない要素に分割する Partition 関数(メソッド)を作ってみました。

using System;
using System.Collections.Generic;
using System.Linq;

class Program {
    static Tuple<List<T>, List<T>> Partition<T>(IEnumerable<T> xs, Predicate<T> pred) {
        var ok = new List<T>();
        var ng = new List<T>();
        foreach (var x in xs) {
            if (pred(x)) {
                ok.Add(x);
            }
            else {
                ng.Add(x);
            }
        }
        return Tuple.Create(ok, ng);
    }

    static void Main() {
        var xs = new[] { 1, 2, 3, 4, 5 };

        // 偶数と奇数に分割する
        var t = Partition(xs, e => e % 2 == 0);
        Console.WriteLine(string.Join(", ", t.Item1)); // 2, 4
        Console.WriteLine(string.Join(", ", t.Item2)); // 1, 3, 5

        // ゼロまたは正と負に分割する
        var t2 = Partition(new[] { 1, -3, 22, -90, -100 }, e => e >= 0);
        Console.WriteLine(string.Join(", ", t2.Item1)); // 1, 22
        Console.WriteLine(string.Join(", ", t2.Item2)); // -3, -90, -100
    }
}

実行結果です。

2, 4
1, 3, 5
1, 22
-3, -90, -100

リンク

Predicate(T) デリゲート (System)

Group 関数

シーケンス内の隣り合う要素のうち、同じ値のものを配列にまとめる関数 Group を作ってみました。

using System;
using System.Collections.Generic;
using System.Linq;

static class Iter {
    public static IEnumerable<T[]> Group<T>(IEnumerable<T> xs) {
        var ls = new List<T>();
        foreach (var x in xs) {
            if (ls.Count == 0 || ls[0].Equals(x)) {
                ls.Add(x);
            }
            else {
                yield return ls.ToArray();
                ls.Clear();
                ls.Add(x);
            }
        }
        if (ls.Count > 0) {
            yield return ls.ToArray();
        }
    }
}

class Program {
    static void Display<T>(IEnumerable<T[]> xxs) {
        var ls = new List<string>();
        foreach (var xs in xxs) {
            ls.Add(string.Format("[{0}]", string.Join(", ", xs)));
        }
        var s = string.Format("[{0}]", string.Join(", ", ls));
        Console.WriteLine(s);
    }

    static void Main() {
        Display(Iter.Group(new[] { 1 }));
        Display(Iter.Group(new[] { 1, 1 }));
        Display(Iter.Group(new int[0]));
        Display(Iter.Group(new[] { 1, 1, 0, 0, 1, 1, 1, 2, 2, 0, 0, 0 }));
        Display(Iter.Group(new[] { 1, 2, 3, 0, 0 }));
    }
}

実行結果です。

[[1]]
[[1, 1]]
[]
[[1, 1], [0, 0], [1, 1, 1], [2, 2], [0, 0, 0]]
[[1], [2], [3], [0, 0]]

対角線のどちら側か

f:id:noriok:20160218073255p:plain

R, C が与えられたとき、それぞれの格子点が対角線のどちら側にあるか判定するプログラムです。

  • 対角線の傾き (R-1)/(C - 1)
  • 格子点の傾き i/j

について、その大きさを比較します。もし傾きが等しければ格子点は対角線上にあることがわかります。 等しくなければ、格子点は対角線の左側(右側)のどちらかにあります。

大きさを比較するときは、割り算を避けるために通分して整数での比較をするようにします。

using System;
using System.Collections.Generic;
using System.Linq;

class Program {
    static void Diagonal(int R, int C) {
        if (R < 2 || C < 2) throw new ArgumentException();

        Console.WriteLine("R:{0} C:{1}", R, C);
        for (int i = 0; i < R; i++) {
            for (int j = 0; j < C; j++) {
                int a = (R-1) * j;
                int b = i * (C-1);
                if (a == b) {
                    Console.Write("o"); // 対角線上
                }
                else if (a < b) {
                    Console.Write("+"); // 対角線の左側
                }
                else {
                    Console.Write("-"); // 対角線の右側
                }
            }
            Console.WriteLine();
        }
        Console.WriteLine();
    }

    static void Main() {
        Diagonal(4, 4);
        Diagonal(3, 5);
        Diagonal(3, 6);
        Diagonal(3, 7);
        Diagonal(2, 4);
    }
}

実行結果です。対角線上にある格子点を o で表し、それ以外を領域ごとに +- で表しています。

R:4 C:4
o---
+o--
++o-
+++o

R:3 C:5
o----
++o--
++++o

R:3 C:6
o-----
+++---
+++++o

R:3 C:7
o------
+++o---
++++++o

R:2 C:4
o---
+++o

ラムダ式で再帰

ラムダ式再帰呼び出しのサンプルです。

using System;

class Program {
    static void Main() {
        Func<int, int> fact = null;
        fact = n => {
            if (n == 0)
                return 1;
            else
                return n * fact(n - 1);
        };

        for (int i = 1; i < 10; i++)
            Console.WriteLine(fact(i));
    }
}

実行結果です。

1
2
6
24
120
720
5040
40320
362880

fact 変数を null で初期化しています。 そうしないと、ラムダ式の中で自身の変数が参照できず、コンパイルエラーになります。

Func<int, int> fact = n => {
    if (n == 0)
        return 1;
    else
        return n * fact(n - 1); // コンパイルエラー: fact が見えない
};

コンパイルエラーメッセージ:

$ mcs test.cs
test.cs(9,28): error CS0165: Use of unassigned local variable `fact'
Compilation failed: 1 error(s), 0 warnings