Table of Contents [Hide/Show]
Chapter 7 - Extending LINQ to Objects Writing New Operators Listing 7-3 : RandomElement operator. Listing 7-4 : RandomElement operator + unit tests. Listing 7-4 to 7-7 : TakeRange operator + unit tests. Listing 7-9 : LongSum operator + unit tests. Segment operator sample 1 Segment operator sample 2 Listing 7-16 - Segment operator over contact records Listing 7-12 : Segment operator + unit tests.
public void Listing_7_3_RandomElement_Operator() { var contacts = Contact.SampleData(); Contact con = contacts.RandomElement(); Console.WriteLine("{0} {1} – {2}", con.FirstName, con.LastName, con.Phone); } public static class Listing_7_3_Extensions { public static T RandomElement<T>( this IEnumerable<T> source, int seed = 0 /* optional parameter, new to C# 4.0 */ ) { // check for invalid source conditions if (source == null) throw new ArgumentNullException("source"); // the Count() extension method already has this // optimization built–in. I'm showing it here to // make the point it is being considered! int count = 0; if (source is ICollection) count = ((ICollection)source).Count; else count = source.Count(); if (count == 0) throw new InvalidOperationException("No elements"); // use the passed in seed value, or use time–based seed Random random; if (seed == 0) random = new Random(); else random = new Random(seed); // IList implementors, access by indexer. its faster. IList<T> list = source as IList<T>; if (list != null) { int index = random.Next(0, count); return list[index]; } else { // Other collection types must be // enumerated (eg. Dictionary, IEnumerable) int index = random.Next(0, count); using (IEnumerator<T> e = source.GetEnumerator()) { // move to the first element (the initial // position is BEFORE the first element. e.MoveNext(); // iterate and move until we hit our index while (index > 0) { e.MoveNext(); index––; } return e.Current; } } } public static T RandomElementOrDefault<T>( this IEnumerable<T> source) { // check for invalid source conditions if (source == null) throw new ArgumentNullException("source"); // the Count() extension method already has this // optimization built–in. I'm showing it here to // make the point it is being considered. int count = 0; if (source is ICollection) count = ((ICollection)source).Count; else count = source.Count(); if (count == 0) return default(T); //... same as RandomElement operator code. return default(T); } }
Stewart Kagel – 546 607 5462
public void Listing_7_4_RandomElement_Operator_UnitTests() { RunUnitTestFixture("SampleQueries.RandomElementUnitTests"); } public static class Listing_7_3_Extensions { public static T RandomElement<T>( this IEnumerable<T> source, int seed = 0 /* optional parameter, new to C# 4.0 */ ) { // check for invalid source conditions if (source == null) throw new ArgumentNullException("source"); // the Count() extension method already has this // optimization built–in. I'm showing it here to // make the point it is being considered! int count = 0; if (source is ICollection) count = ((ICollection)source).Count; else count = source.Count(); if (count == 0) throw new InvalidOperationException("No elements"); // use the passed in seed value, or use time–based seed Random random; if (seed == 0) random = new Random(); else random = new Random(seed); // IList implementors, access by indexer. its faster. IList<T> list = source as IList<T>; if (list != null) { int index = random.Next(0, count); return list[index]; } else { // Other collection types must be // enumerated (eg. Dictionary, IEnumerable) int index = random.Next(0, count); using (IEnumerator<T> e = source.GetEnumerator()) { // move to the first element (the initial // position is BEFORE the first element. e.MoveNext(); // iterate and move until we hit our index while (index > 0) { e.MoveNext(); index––; } return e.Current; } } } public static T RandomElementOrDefault<T>( this IEnumerable<T> source) { // check for invalid source conditions if (source == null) throw new ArgumentNullException("source"); // the Count() extension method already has this // optimization built–in. I'm showing it here to // make the point it is being considered. int count = 0; if (source is ICollection) count = ((ICollection)source).Count; else count = source.Count(); if (count == 0) return default(T); //... same as RandomElement operator code. return default(T); } } public class RandomElementUnitTests { //3456789|123456789|123456789|123456789|123456789|12345678 [Test] [ExpectedException("System.ArgumentNullException")] public void NullSourceTest() { int[] values = null; int result = values.RandomElement(); } [Test] [ExpectedException("System.InvalidOperationException")] public void EmptySourceTest() { var values = Enumerable.Empty<int>(); int result = values.RandomElement(); } [Test] public void IEnumerableTest() { // create a test enumerable of int's 0–999 var values = Enumerable.Range(0, 1000); int result1 = values.RandomElement(1); // Random class seed is different var values2 = Enumerable.Range(0, 1000); int result2 = values2.RandomElement(2); // ensure a different result is returned // it is OK for this operator to return the same // element. However, we only have a 1 in 1000 chance. Assert.AreNotSame(result1, result2); } [Test] public void IListTest() { // create a test array of int's 0–999 var values = Enumerable.Range(0, 1000).ToArray(); int result1 = values.RandomElement(1); // use a different random seed int result2 = values.RandomElement(2); // it is OK for this operator to return the same // element. However, we only have a 1 in 1000 chance. Assert.AreNotSame(result1, result2); } [Test] public void SingleElementTest() { // for a single element source, // it should be returned every time // IEnumerable<T> var values = Enumerable.Range(10, 1); int result1 = values.RandomElement(1); Assert.AreEqual(10, result1); // IList<T> var values2 = values.ToArray(); int result2 = values.RandomElement(2); Assert.AreEqual(10, result2); } [Test] public void BoundaryEnumerableTest() { // for a two element source, we want // to check the boundaries can be returned var values = Enumerable.Range(10, 2); // check both results are returned at // least once in 25 attempts using // different random number generator seeds. int result1 = values.RandomElement(); bool foundDifferent = false; for (int i = 0; i < 25; i++) { int result2 = values.RandomElement(i+100); if (result1 != result2) { foundDifferent = true; break; } } Assert.IsTrue(foundDifferent); } [Test] public void BoundaryIListTest() { // for a two element source, we want // to check the boundaries can be returned var values = Enumerable.Range(10, 2).ToArray(); // check both results are returned at // least once in 25 attempts using // different random number generator seeds. int result1 = values.RandomElement(); bool foundDifferent = false; for (int i = 0; i < 25; i++) { int result2 = values.RandomElement(i+100); if (result1 != result2) { foundDifferent = true; break; } } Assert.IsTrue(foundDifferent); } }
PASS: NullSourceTest PASS: EmptySourceTest PASS: IEnumerableTest PASS: IListTest PASS: SingleElementTest PASS: BoundaryEnumerableTest PASS: BoundaryIListTest
public void Listing_7_7_TakeRange_Operator_UnitTests() { RunUnitTestFixture("SampleQueries.TakeRangeUnitTests"); } public static class Listing_7_4_to_7_6_Extensions { private static IEnumerable<T> TakeRangeIterator<T>( this IEnumerable<T> source, Func<T, bool> startPredicate, Func<T, bool> endPredicate) { // check for invalid input argument conditions if (source == null) throw new ArgumentNullException("source"); if (startPredicate == null) throw new ArgumentNullException("startPredicate"); if (endPredicate == null) throw new ArgumentNullException("endPredicate"); // begin the iteration ... bool foundStart = false; foreach (T element in source) { if (startPredicate(element)) foundStart = true; if (foundStart) if (endPredicate(element)) yield break; else yield return element; } } public static IEnumerable<T> TakeRange<T>( this IEnumerable<T> source, Func<T, bool> endPredicate) { return TakeRangeIterator<T>( source, b => true, endPredicate); } public static IEnumerable<T> TakeRange<T>( this IEnumerable<T> source, Func<T, bool> startPredicate, Func<T, bool> endPredicate) { return TakeRangeIterator<T>( source, startPredicate, endPredicate); } } public class TakeRangeUnitTests { //3456789|123456789|123456789|123456789|123456789|12345678 [Test] [ExpectedException("System.ArgumentNullException")] public void NullSourceTest() { int[] values = null; var result = values.TakeRange( start => true, end => true).ToArray<int>(); } [Test] [ExpectedException("System.ArgumentNullException")] public void NullStartPredicateTest() { int[] values = null; int[] result = values.TakeRange( null, b => true).ToArray<int>(); } [Test] [ExpectedException("System.ArgumentNullException")] public void NullEndPredicateTest() { int[] values = null; int[] result = values.TakeRange( a => true, null).ToArray<int>(); } [Test] public void IntArrayTest() { var values = Enumerable.Range(0, 20); int[] result = values.TakeRange( start => start > 4, end => end > 9).ToArray<int>(); Assert.AreEqual( 5, result.Count(), "incorrect count returned"); // expecting 5,6,7,8,9 to be returned for (int i = 0; i < result.Count(); i++) Assert.AreEqual(i + 5, result[i]); } [Test] public void IntArrayEndPredicateOnlyTest() { var values = Enumerable.Range(0, 20); int[] result = values.TakeRange( end => end > 9).ToArray<int>(); Assert.AreEqual( 10, result.Count(), "incorrect count returned"); // expecting 10–19 to be returned for (int i = 10; i < result.Count(); i++) Assert.AreEqual(i, result[i]); } }
PASS: NullSourceTest PASS: NullStartPredicateTest PASS: NullEndPredicateTest PASS: IntArrayTest PASS: IntArrayEndPredicateOnlyTest
public void Listing_7_9_LongSum_Operator_UnitTests() { RunUnitTestFixture("SampleQueries.LongSumUnitTests"); } public static class Listing_7_8_Extensions { public static long LongSum( this IEnumerable<int> source) { if (source == null) throw new ArgumentNullException("source"); long sum = 0; checked { foreach (int v in source) sum += v; } return sum; } public static long LongSum<T>( this IEnumerable<T> source, Func<T, int> selector) { return source .Select(selector) .LongSum(); } } public class LongSumUnitTests { [Test] public void LongSumIntTest() { int[] values = new int[] { 5, 4, 3, 2, 1, 6, 7, 8, 9, 0 }; long result = values.LongSum(); Assert.AreEqual(45, result, "LongSum incorrect result."); } [Test] public void LongSumNegativeIntTest() { int[] values = new int[] { int.MinValue, int.MaxValue, 1 }; long result = values.LongSum(); Assert.AreEqual(0, result, "Negative value LongSum incorrect result."); } [Test] public void LongSumIntBigTest() { int[] values = new int[] { int.MaxValue, 1, int.MaxValue }; long result = values.LongSum(); long correct = (long)int.MaxValue + (long)int.MaxValue + 1; Assert.AreEqual(correct, result); } [Test] [ExpectedException(typeof(ArgumentNullException))] public void LongSumIntNullSourceTest() { int[] values = null; long result = values.LongSum(); } [Test] public void LongSumIntEmptySourceTest() { var values = Enumerable.Empty<int>(); long result = values.LongSum(); Assert.AreEqual(0, result); } [Test] public void LongSumIntWithSelectorTest() { int[] values = new int[] { 5, 4, 3, 2, 1, 6, 7, 8, 9, 0 }; long result = values.LongSum(i => i + 1); Assert.AreEqual(55, result); } }
PASS: LongSumIntTest PASS: LongSumNegativeIntTest PASS: LongSumIntBigTest PASS: LongSumIntNullSourceTest PASS: LongSumIntEmptySourceTest PASS: LongSumIntWithSelectorTest
public void Listing_7_x_Segment_Operator_Sample_1() { string[] elements = new string[] { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J" }; var groups = elements.Segment(5); foreach (var g in groups) { Console.WriteLine("Group key: {0}", g.Key); foreach (var elm in g) Console.WriteLine(" {0}", elm); } }
Group key: 1 A B Group key: 2 C D Group key: 3 E F Group key: 4 G H Group key: 5 I J
public void Listing_7_x_Segment_Operator_Sample_2() { string[] elements = new string[] { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J" }; var groups = elements.Segment(4); foreach (var g in groups) { Console.WriteLine("Group key: {0}", g.Key); foreach (var elm in g) Console.WriteLine(" {0}", elm); } }
Group key: 1 A B C Group key: 2 D E F Group key: 3 G H I Group key: 4 J
public void Listing_7_16_Segment_Over_Contacts() { List<Contact> contacts = Contact.SampleData(); // split the contacts into 2 groups for A/B testing var groups = contacts.Segment(2); foreach (var g in groups) { Console.WriteLine("Group key: {0}", g.Key); foreach (var elm in g) Console.WriteLine(" {0}, {1}", elm.LastName.ToUpper(), elm.FirstName); } }
Group key: 1 GOTTSHALL, Barney VALDES, Armando GAUWAIN, Adam DEANE, Jeffery ZEEMAN, Collin Group key: 2 KAGEL, Stewart LARD, Chance REIFSTECK, Blaine KAMPH, Mack HAZELGROVE, Ariel
public void Listing_7_12_Segment_Operator_UnitTests() { RunUnitTestFixture("SampleQueries.SegmentUnitTests"); } public static class Listing_7_11_Extensions { public static IEnumerable<IGrouping<int, T>> Segment<T>( this IEnumerable<T> source, int segments) { if (source == null) throw new ArgumentNullException("source"); if (segments <= 0) throw new ArgumentOutOfRangeException("segments"); return SegmentIterator<T>(source, segments); } private static IEnumerable<IGrouping<int, T>> SegmentIterator<T>( IEnumerable<T> source, int segments) { // calculate the number of elements per segment int count = source.Count(); int perSegment = (int)Math.Ceiling( (decimal)count / segments); // build the empty groups Grouping<int, T>[] groups = new Grouping<int, T>[segments]; for (int i = 0; i < segments; i++) { Grouping<int, T> g = new Grouping<int, T>(perSegment); g.key = i + 1; groups[i] = g; } // fill the groups and yield results // when each group full. int index = 0; int segment = 1; Grouping<int, T> group = groups[0]; using (IEnumerator<T> e = source.GetEnumerator()) { while (e.MoveNext()) { group.Add(e.Current); index++; // yield return when we have filled each group if ((segment < segments) && (index == perSegment)) { yield return group; index = 0; segment++; group = groups[segment – 1]; } } } // return the last and any remaining groups // (these will be empty or partially populated) while (segment <= segments) { yield return groups[segment – 1]; segment++; } } } public class SegmentUnitTests { [Test] [ExpectedException("System.ArgumentNullException")] public void NullSourceTest() { int[] values = null; var result = values.Segment(1); } [Test] public void EmptySourceTest() { // expecting 4 empty groups int[] values = Enumerable.Empty<int>().ToArray(); var result = values.Segment(4); Assert.AreEqual(4, result.Count()); } [Test] public void EvenSegmentTest() { int[] values = Enumerable.Range(1, 100).ToArray<int>(); var result = values.Segment(4); Assert.AreEqual(4, result.Count()); foreach (var g in result) Assert.AreEqual(25, g.Count()); } [Test] public void MoreSegmentsThanElementsTest() { int[] values = Enumerable.Range(1, 3).ToArray<int>(); var result = values.Segment(10); Assert.AreEqual(10, result.Count()); int i = 1; foreach (var g in result) { if (i < 4) Assert.AreEqual(1, g.Count()); else Assert.AreEqual(0, g.Count()); i++; } } [Test] public void OddSegmentTest() { int[] values = Enumerable.Range(1, 101).ToArray<int>(); var result = values.Segment(4); Assert.AreEqual(4, result.Count()); int i = 1; foreach (var g in result) { if (i < 4) Assert.AreEqual(26, g.Count()); else Assert.AreEqual(23, g.Count()); i++; } } }
PASS: NullSourceTest PASS: EmptySourceTest PASS: EvenSegmentTest PASS: MoreSegmentsThanElementsTest PASS: OddSegmentTest