วันพฤหัสบดีที่ 15 พฤษภาคม พ.ศ. 2551

Extension Methods for Enumerable Part II - Index injection and index extraction

In enumerable type, index is useful to identify a member. In C# 3.0, some methods for enumerable provide index parameter such as Select and Where. But most of methods are still not provide index, such as GroupBy and OrderBy.

One of the easy way to inject index into enumerable is to use Closure. For example, if you want to order "ABCDEFG" alternately you may write.

int i = 0;
var e = "ABCDEFG".OrderBy(c => i++ % 2);
Console.WriteLine(e.ListMembers());
//A C E G B D F


To inject an index, you may declare a variable (i) and use this variable as an index in the delegate. You must also put incremental operator (++) to index variable to make index increase every iteration.

Anyway, using of Closure also has drawback. The index variable will not be cleared after enumeration. When you enumerate again index variable will be incorrect. For example:

int i = 0;
var e = "ABCDEFG".OrderBy(c => i++ % 2);
Console.WriteLine(e.ListMembers());
//A C E G B D F
Console.WriteLine(e.ListMembers());
//B D F A C E G


As you can see, the third command and the forth command are the same. But the result is not the same. Because, after run the third command, i value is equal to 7. Then when you call the forth command, the index will be started at index 7 and so on. Finally, the result will not be the same for the second time.

Anyway, this issue can be solved by make the class persistent.

int i = 0;
var e = "ABCDEFG".OrderBy(c => i++ % 2).ToArray();
Console.WriteLine(e.ListMembers());
//A C E G B D F
Console.WriteLine(e.ListMembers());
//A C E G B D F


Above example, we call ToArray after OrderBy method, and make e variable persistent. The result for e will be the same everytime.

Another solution for index injection is to create extension methods.

public static Dictionary<int, T> WithIndex<T>(this IEnumerable<T> source) {
    return source.WithIndex(0.To(int.MaxValue), EqualityComparer<int>.Default);
}
public static Dictionary<T1, T2> WithIndex<T1, T2>(this IEnumerable<T2> source, IEnumerable<T1> index) {
    return source.WithIndex(index, EqualityComparer<T1>.Default);
}
public static Dictionary<T1, T2> WithIndex<T1, T2>(this IEnumerable<T2> source, IEnumerable<T1> index, IEqualityComparer<T1> comparer) {
    Dictionary<T1, T2> dict = new Dictionary<T1, T2>(comparer);
    using (IEnumerator<T2> source_enum = source.GetEnumerator()) {
        using (IEnumerator<T1> index_enum = index.GetEnumerator()) {
            while (source_enum.MoveNext()) {
                if (!index_enum.MoveNext())
                    throw new ArgumentException("index cannot has less length than source", "index");
                dict.Add(index_enum.Current, source_enum.Current);
            }
        }
    }
    return dict;
}


You just call above method before enumeration. The result you get is a dictionary. Key is index, and Value is object. Then after enumeration, you may call Select method to remove index from the object. For example:

var e = from c in "ABCDEFG".WithIndex()
        orderby c.Key % 2
        select c.Value;
Console.WriteLine(e.ListMembers());
//A C E G B D F
Console.WriteLine(e.ListMembers());
//A C E G B D F


You can also pass any kind of index into WithIndex method. The index's length must be greater than or equal to source's length.

For example, you have source and position. And you want to sort source by position. In this case, you can assign position as an index of the source.

var e = from x in source.WithIndex(position)
        orderby x.Key
        select x.Value;


Index extraction is index from specific item in the enumerable. Here is the extension methods:

public static int IndexOf<T>(this IEnumerable<T> source, T item) {
    return source.IndexOf(item, EqualityComparer<T>.Default);
}
public static int IndexOf<T>(this IEnumerable<T> source, T item, IEqualityComparer<T> comparer) {
    IList<T> list = source as IList<T>;
    if (list != null)
        return list.IndexOf(item);

    int i = 0;
    foreach (T x in source) {
        if (comparer.Equals(x, item))
            return i;
    }
    return -1;
}


The example for IndexOf is from CodeGolf. The question is Reverse game. You will get list of numbers randomly. And you must tell how many numbers from the left to reverse. And target is to arrage the numbers.

int[] list = new[] { 8, 9, 4, 5, 1, 3, 6, 2, 7 };

Action<int> reverse = i => {
    if (i > 1) {
        Console.WriteLine("reverse: {0}", i);
        list = list.Take(i).Reverse().Concat(list.Skip(i)).ToArray();
        Console.WriteLine(list.ListMembers());
    }
};

list.Length.DownTo(1).ForEach(x => {
    if (x != list[x - 1]) {
        reverse(list.IndexOf(x) + 1);
        reverse(x);
    }
});
//8 9 4 5 1 3 6 2 7
//reverse: 2
//9 8 4 5 1 3 6 2 7
//reverse: 9
//7 2 6 3 1 5 4 8 9
//reverse: 7
//4 5 1 3 6 2 7 8 9
//reverse: 5
//6 3 1 5 4 2 7 8 9
//reverse: 6
//2 4 5 1 3 6 7 8 9
//reverse: 3
//5 4 2 1 3 6 7 8 9
//reverse: 5
//3 1 2 4 5 6 7 8 9
//reverse: 3
//2 1 3 4 5 6 7 8 9
//reverse: 2
//1 2 3 4 5 6 7 8 9


Above example, you need IndexOf to identify position of the next number you want to reverse in the list. Then you reverse that number to the left, and then you reverse again to move the number to appropriate position.

Next, we will go to Roman Numerals.

ไม่มีความคิดเห็น: