EditChapter 7 - Extending LINQ to Objects
WARNING: Some of the Listing numbers DON'T MATCH the printed publication. I'll be synchronizing them as soon as possible, until then ignore the listing numbers. The code is the same, it is just the Listing numbers that do not match.EditWriting New Operators
EditListing 7-3 : RandomElement operator.
This sample shows how to write a Single element operator. The RandomElement operator returns a single element at random from a source sequence. This method demonstrates its usage.
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);
}
}
Console output (Execution time: 9ms):
[
Hide/Show]
Stewart Kagel – 546 607 5462
Top
EditListing 7-4 : RandomElement operator + unit tests.
This sample shows how to write a Single element operator. The RandomElement operator returns a single element at random from a source sequence. This method runs the unit-tests for the operator.
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);
}
}
Console output (Execution time: 189ms):
[
Hide/Show]
PASS: NullSourceTest
PASS: EmptySourceTest
PASS: IEnumerableTest
PASS: IListTest
PASS: SingleElementTest
PASS: BoundaryEnumerableTest
PASS: BoundaryIListTest
Top
EditListing 7-4 to 7-7 : TakeRange operator + unit tests.
This sample shows how to write a Sequence operator. The TakeRange operator returns all elements starting with the first element to pass the Start predicate, and ending with the last element that passes the End predicate. This method runs the unit-tests for the operator.
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]);
}
}
Console output (Execution time: 16ms):
[
Hide/Show]
PASS: NullSourceTest
PASS: NullStartPredicateTest
PASS: NullEndPredicateTest
PASS: IntArrayTest
PASS: IntArrayEndPredicateOnlyTest
Top
EditListing 7-9 : LongSum operator + unit tests.
This sample shows how to write a Aggregate operator. The LongSum operator returns the sum of a sequence of integers as a long type. This method runs the unit-tests for the operator.
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);
}
}
Console output (Execution time: 21ms):
[
Hide/Show]
PASS: LongSumIntTest
PASS: LongSumNegativeIntTest
PASS: LongSumIntBigTest
PASS: LongSumIntNullSourceTest
PASS: LongSumIntEmptySourceTest
PASS: LongSumIntWithSelectorTest
Top
EditSegment operator sample 1
.
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);
}
}
Console output (Execution time: 29ms):
[
Hide/Show]
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
Top
EditSegment operator sample 2
.
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);
}
}
Console output (Execution time: 1ms):
[
Hide/Show]
Group key: 1
A
B
C
Group key: 2
D
E
F
Group key: 3
G
H
I
Group key: 4
J
Top
EditListing 7-16 - Segment operator over contact records
Demonstrate using the segment operator by creating two test groups from contact records.
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);
}
}
Console output (Execution time: 2ms):
[
Hide/Show]
Top
EditListing 7-12 : Segment operator + unit tests.
This sample shows how to write a Grouping operator. The Segment operator returns the number of requested groups (segments argument), populated with an equal number of elements (where possible). This method runs the unit-tests for the operator.
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++;
}
}
}
Console output (Execution time: 16ms):
[
Hide/Show]
PASS: NullSourceTest
PASS: EmptySourceTest
PASS: EvenSegmentTest
PASS: MoreSegmentsThanElementsTest
PASS: OddSegmentTest
Top