From 6024d5d237e83a5e08daa6467229216af9765286 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Thu, 20 Dec 2018 15:20:04 +0100 Subject: [PATCH 1/4] Add initial implementation of BindByIndex --- MoreLinq/BindByIndex.cs | 218 ++++++++++++++++++ MoreLinq/Extensions.g.cs | 54 +++++ .../PublicAPI/net6.0/PublicAPI.Unshipped.txt | 5 + .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 5 + .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 5 + .../netstandard2.0/PublicAPI.Unshipped.txt | 5 + .../netstandard2.1/PublicAPI.Unshipped.txt | 5 + 7 files changed, 297 insertions(+) create mode 100644 MoreLinq/BindByIndex.cs diff --git a/MoreLinq/BindByIndex.cs b/MoreLinq/BindByIndex.cs new file mode 100644 index 000000000..e707db6e1 --- /dev/null +++ b/MoreLinq/BindByIndex.cs @@ -0,0 +1,218 @@ +#region License and Terms +// MoreLINQ - Extensions to LINQ to Objects +// Copyright (c) 2018 Atif Aziz. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#endregion + +namespace MoreLinq +{ + using System; + using System.Collections; + using System.Collections.Generic; + + static partial class MoreEnumerable + { + /// + /// TODO Complete documentation + /// + /// + /// Type of elements in sequence. + /// Type of result elements returned. + /// The source sequence. + /// The sequence of indices. + /// + /// TODO Complete documentation + /// + /// + /// TODO Complete documentation + /// + /// + /// TODO Complete documentation + /// + + public static IEnumerable + BindByIndex(this IEnumerable source, IEnumerable indices, + Func missingSelector, Func resultSelector) => + BindByIndex(source, indices, null, missingSelector, resultSelector); + + /// + /// TODO Complete documentation + /// + /// + /// Type of elements in sequence. + /// Type of result elements returned. + /// The source sequence. + /// The sequence of indices. + /// Size of look-back buffer. + /// + /// TODO Complete documentation + /// + /// + /// TODO Complete documentation + /// + /// + /// TODO Complete documentation + /// + + public static IEnumerable + BindByIndex(this IEnumerable source, IEnumerable indices, + int lookBackSize, + Func missingSelector, + Func resultSelector) => + BindByIndex(source, indices, (int?)lookBackSize, missingSelector, resultSelector); + + static IEnumerable + BindByIndex(IEnumerable source, IEnumerable indices, + int? lookBackSize, + Func missingSelector, + Func resultSelector) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + if (indices == null) throw new ArgumentNullException(nameof(indices)); + if (lookBackSize < 0) throw new ArgumentOutOfRangeException(nameof(lookBackSize)); + if (missingSelector == null) throw new ArgumentNullException(nameof(missingSelector)); + if (resultSelector == null) throw new ArgumentNullException(nameof(resultSelector)); + + // TODO A version optimized for lists + + return _(lookBackSize switch + { + { } lbs and > 0 => new Queue(lbs, lbs), + 0 => null, + _ => new Queue() + }); + + IEnumerable _(Queue? queue) + { + using var rie = indices.GetEnumerator(); + if (!rie.MoveNext()) + yield break; + + while (rie.Current < 0) + { + yield return missingSelector(rie.Current); + if (!rie.MoveNext()) + yield break; + } + + var ri = rie.Current; + var si = 0; + + foreach (var item in source) + { + while (si == ri) + { + yield return resultSelector(item, si); + do + { + if (!rie.MoveNext()) + yield break; + ri = rie.Current; + if (ri < si) + { + if (queue is { } q && si - q.Count is var qi && ri >= qi) + yield return resultSelector(q[ri - qi], ri); + else + yield return missingSelector(ri); + } + } + while (ri < si); + } + + queue?.Enqueue(item); + si++; + } + + if (ri != si) + { + yield return missingSelector(ri); + while (rie.MoveNext()) + yield return missingSelector(rie.Current); + } + } + } + + /// + /// A queue implementation similar to + /// but which + /// supports a maximum count (exceeding which will cause an item to be + /// dequeued each to make space for a new one being queued) as well as + /// directly indexing into the queue to retrieve any one item. + /// + + sealed class Queue(int maxCount = 0, int capacity = 0) : IReadOnlyList + { + T[] items = capacity > 0 ? new T[capacity] : []; + int firstIndex; + readonly int maxCount = maxCount; + + int Capacity => this.items.Length; + public int Count { get; private set; } + + T IReadOnlyList.this[int index] => this[index]; + + public T this[int index] + { + get + { + if (index < 0 || index >= Count) + { + #pragma warning disable CA2201 // Do not raise reserved exception types + throw new IndexOutOfRangeException(); + #pragma warning restore CA2201 + } + + return Cell(index); + } + } + + ref T Cell(int index) => ref this.items[(this.firstIndex + index) % Capacity]; + + public void Enqueue(T item) + { + if (this.maxCount > 0 && Count == this.maxCount) + _ = Dequeue(); + + if (Count == Capacity) + { + var array = new T[Math.Max(4, Capacity * 2)]; + for (var i = 0; i < Count; i++) + array[i] = this[i]; + this.firstIndex = 0; + this.items = array; + } + + Cell(Count++) = item; + } + + public T Dequeue() + { + if (Count == 0) + throw new InvalidOperationException(); + var result = this[0]; + this.firstIndex++; + --Count; + return result; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < Count; i++) + yield return this[i]; + } + } + } +} diff --git a/MoreLinq/Extensions.g.cs b/MoreLinq/Extensions.g.cs index 52f76097c..f58837a88 100644 --- a/MoreLinq/Extensions.g.cs +++ b/MoreLinq/Extensions.g.cs @@ -716,6 +716,60 @@ public static IEnumerable Batch(this IEnumerableBindByIndex extension. + + [GeneratedCode("MoreLinq.ExtensionsGenerator", "1.0.0.0")] + public static partial class BindByIndexExtension + { + /// + /// TODO Complete documentation + /// + /// + /// Type of elements in sequence. + /// Type of result elements returned. + /// The source sequence. + /// The sequence of indices. + /// + /// TODO Complete documentation + /// + /// + /// TODO Complete documentation + /// + /// + /// TODO Complete documentation + /// + + public static IEnumerable + BindByIndex(this IEnumerable source, IEnumerable indices, + Func missingSelector, Func resultSelector) => MoreEnumerable. BindByIndex(source, indices, missingSelector, resultSelector); + + /// + /// TODO Complete documentation + /// + /// + /// Type of elements in sequence. + /// Type of result elements returned. + /// The source sequence. + /// The sequence of indices. + /// Size of look-back buffer. + /// + /// TODO Complete documentation + /// + /// + /// TODO Complete documentation + /// + /// + /// TODO Complete documentation + /// + + public static IEnumerable + BindByIndex(this IEnumerable source, IEnumerable indices, + int lookBackSize, + Func missingSelector, + Func resultSelector) => MoreEnumerable. BindByIndex(source, indices, lookBackSize, missingSelector, resultSelector); + + } + /// Cartesian extension. [GeneratedCode("MoreLinq.ExtensionsGenerator", "1.0.0.0")] diff --git a/MoreLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt b/MoreLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt index 7dc5c5811..c5f2f242d 100644 --- a/MoreLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt +++ b/MoreLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ #nullable enable +MoreLinq.Extensions.BindByIndexExtension +static MoreLinq.Extensions.BindByIndexExtension.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! +static MoreLinq.Extensions.BindByIndexExtension.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, int lookBackSize, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, int lookBackSize, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! diff --git a/MoreLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/MoreLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 7dc5c5811..c5f2f242d 100644 --- a/MoreLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/MoreLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ #nullable enable +MoreLinq.Extensions.BindByIndexExtension +static MoreLinq.Extensions.BindByIndexExtension.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! +static MoreLinq.Extensions.BindByIndexExtension.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, int lookBackSize, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, int lookBackSize, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! diff --git a/MoreLinq/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/MoreLinq/PublicAPI/net9.0/PublicAPI.Unshipped.txt index 7dc5c5811..c5f2f242d 100644 --- a/MoreLinq/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/MoreLinq/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ #nullable enable +MoreLinq.Extensions.BindByIndexExtension +static MoreLinq.Extensions.BindByIndexExtension.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! +static MoreLinq.Extensions.BindByIndexExtension.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, int lookBackSize, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, int lookBackSize, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! diff --git a/MoreLinq/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/MoreLinq/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 7dc5c5811..c5f2f242d 100644 --- a/MoreLinq/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/MoreLinq/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ #nullable enable +MoreLinq.Extensions.BindByIndexExtension +static MoreLinq.Extensions.BindByIndexExtension.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! +static MoreLinq.Extensions.BindByIndexExtension.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, int lookBackSize, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, int lookBackSize, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! diff --git a/MoreLinq/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt b/MoreLinq/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt index 7dc5c5811..c5f2f242d 100644 --- a/MoreLinq/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt +++ b/MoreLinq/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ #nullable enable +MoreLinq.Extensions.BindByIndexExtension +static MoreLinq.Extensions.BindByIndexExtension.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! +static MoreLinq.Extensions.BindByIndexExtension.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, int lookBackSize, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.BindByIndex(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEnumerable! indices, int lookBackSize, System.Func! missingSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! From b1af01ee5dc9c6fa437867754a1898d3adbc25bc Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 4 Jan 2025 11:55:02 +0100 Subject: [PATCH 2/4] Convert queue into a file-scoped class --- MoreLinq/BindByIndex.cs | 110 ++++++++++++++++++++-------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/MoreLinq/BindByIndex.cs b/MoreLinq/BindByIndex.cs index e707db6e1..b672d7712 100644 --- a/MoreLinq/BindByIndex.cs +++ b/MoreLinq/BindByIndex.cs @@ -142,77 +142,77 @@ IEnumerable _(Queue? queue) } } } + } - /// - /// A queue implementation similar to - /// but which - /// supports a maximum count (exceeding which will cause an item to be - /// dequeued each to make space for a new one being queued) as well as - /// directly indexing into the queue to retrieve any one item. - /// - - sealed class Queue(int maxCount = 0, int capacity = 0) : IReadOnlyList - { - T[] items = capacity > 0 ? new T[capacity] : []; - int firstIndex; - readonly int maxCount = maxCount; - - int Capacity => this.items.Length; - public int Count { get; private set; } - - T IReadOnlyList.this[int index] => this[index]; + /// + /// A queue implementation similar to + /// but which + /// supports a maximum count (exceeding which will cause an item to be + /// dequeued each to make space for a new one being queued) as well as + /// directly indexing into the queue to retrieve any one item. + /// - public T this[int index] - { - get - { - if (index < 0 || index >= Count) - { - #pragma warning disable CA2201 // Do not raise reserved exception types - throw new IndexOutOfRangeException(); - #pragma warning restore CA2201 - } + file sealed class Queue(int maxCount = 0, int capacity = 0) : IReadOnlyList + { + T[] items = capacity > 0 ? new T[capacity] : []; + int firstIndex; + readonly int maxCount = maxCount; - return Cell(index); - } - } + int Capacity => this.items.Length; + public int Count { get; private set; } - ref T Cell(int index) => ref this.items[(this.firstIndex + index) % Capacity]; + T IReadOnlyList.this[int index] => this[index]; - public void Enqueue(T item) + public T this[int index] + { + get { - if (this.maxCount > 0 && Count == this.maxCount) - _ = Dequeue(); - - if (Count == Capacity) + if (index < 0 || index >= Count) { - var array = new T[Math.Max(4, Capacity * 2)]; - for (var i = 0; i < Count; i++) - array[i] = this[i]; - this.firstIndex = 0; - this.items = array; +#pragma warning disable CA2201 // Do not raise reserved exception types + throw new IndexOutOfRangeException(); +#pragma warning restore CA2201 } - Cell(Count++) = item; + return Cell(index); } + } - public T Dequeue() - { - if (Count == 0) - throw new InvalidOperationException(); - var result = this[0]; - this.firstIndex++; - --Count; - return result; - } + ref T Cell(int index) => ref this.items[(this.firstIndex + index) % Capacity]; - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public void Enqueue(T item) + { + if (this.maxCount > 0 && Count == this.maxCount) + _ = Dequeue(); - public IEnumerator GetEnumerator() + if (Count == Capacity) { + var array = new T[Math.Max(4, Capacity * 2)]; for (var i = 0; i < Count; i++) - yield return this[i]; + array[i] = this[i]; + this.firstIndex = 0; + this.items = array; } + + Cell(Count++) = item; + } + + public T Dequeue() + { + if (Count == 0) + throw new InvalidOperationException(); + var result = this[0]; + this.firstIndex++; + --Count; + return result; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < Count; i++) + yield return this[i]; } } } From 20e2d840f7d011001370f63beb6faa084abbcbc1 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 4 Jan 2025 11:55:24 +0100 Subject: [PATCH 3/4] Revise queue summary --- MoreLinq/BindByIndex.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/MoreLinq/BindByIndex.cs b/MoreLinq/BindByIndex.cs index b672d7712..b7af46cf7 100644 --- a/MoreLinq/BindByIndex.cs +++ b/MoreLinq/BindByIndex.cs @@ -145,11 +145,10 @@ IEnumerable _(Queue? queue) } /// - /// A queue implementation similar to - /// but which - /// supports a maximum count (exceeding which will cause an item to be - /// dequeued each to make space for a new one being queued) as well as - /// directly indexing into the queue to retrieve any one item. + /// A queue implementation similar to but + /// which supports a maximum count (exceeding which will cause an item to be dequeued to make + /// space for a new one being queued) as well as directly indexing into the queue to retrieve + /// any one item. /// file sealed class Queue(int maxCount = 0, int capacity = 0) : IReadOnlyList From 62184921500896849e66a5d9bfcb0237362902d6 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 4 Jan 2025 12:14:07 +0100 Subject: [PATCH 4/4] Add unit tests --- MoreLinq.Test/BindByIndexTest.cs | 128 +++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 MoreLinq.Test/BindByIndexTest.cs diff --git a/MoreLinq.Test/BindByIndexTest.cs b/MoreLinq.Test/BindByIndexTest.cs new file mode 100644 index 000000000..f0d50770c --- /dev/null +++ b/MoreLinq.Test/BindByIndexTest.cs @@ -0,0 +1,128 @@ +#region License and Terms +// MoreLINQ - Extensions to LINQ to Objects +// Copyright (c) 2018 Atif Aziz. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#endregion + +namespace MoreLinq.Test +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Text.RegularExpressions; + using MoreLinq.Extensions; + using NUnit.Framework; + + public class BindByIndexTest + { + [TestCase(new[] { 2, 3 }, ExpectedResult = new[] { "baz", "qux" })] + [TestCase(new[] { 1, 3 }, ExpectedResult = new[] { "bar", "qux" })] + [TestCase(new[] { 0, 2, 3 }, ExpectedResult = new[] { "foo", "baz", "qux" })] + [TestCase(new[] { 0, 1, 1 }, ExpectedResult = new[] { "foo", "bar", "bar" })] + [TestCase(new[] { 3, 1, 2 }, ExpectedResult = new[] { "qux", "bar", "baz" })] + [TestCase(new[] { -1, 1, 2, 10 }, ExpectedResult = new[] { "?-1", "bar", "baz", "?10" })] + public string[] WithoutSpecificLookBackSize(IEnumerable indices) + { + using var source = TestingSequence.Of("foo", "bar", "baz", "qux"); + return source.BindByIndex(indices, i => $"?{i.ToInvariantString()}", (s, _) => s) + .ToArray(); + } + + [TestCase(0, new[] { 2, 3 }, ExpectedResult = new[] { "baz", "qux" })] + [TestCase(0, new[] { 1, 3 }, ExpectedResult = new[] { "bar", "qux" })] + [TestCase(0, new[] { 0, 2, 3 }, ExpectedResult = new[] { "foo", "baz", "qux" })] + [TestCase(0, new[] { 0, 1, 1 }, ExpectedResult = new[] { "foo", "bar", "bar" })] + [TestCase(0, new[] { 3, 1, 2 }, ExpectedResult = new[] { "qux", "?1", "?2" })] + [TestCase(4, new[] { 3, 1, 2 }, ExpectedResult = new[] { "qux", "bar", "baz" })] + [TestCase(1, new[] { 3, 1, 2 }, ExpectedResult = new[] { "qux", "?1", "baz" })] + [TestCase(0, new[] { -1, 1, 2, 10 }, ExpectedResult = new[] { "?-1", "bar", "baz", "?10" })] + public string[] WithSpecificLookBackSize(int lookBackSize, IEnumerable indices) + { + using var source = TestingSequence.Of("foo", "bar", "baz", "qux"); + return source.BindByIndex(indices, lookBackSize, i => $"?{i.ToInvariantString()}", (s, _) => s) + .ToArray(); + } + + [Test] + public void ParsingExample() + { + const string csv = """ + # Generated using https://mockaroo.com/ + id,first_name,last_name,email,gender,ip_address + 1,Maggee,Hould,mhould0@ft.com,Female,158.221.234.250 + 2,Judas,Vedekhov,jvedekhov1@google.co.uk,Male,26.25.8.252 + 3,Sharity,Desquesnes,sdesquesnes2@accuweather.com,Female,27.224.140.230 + 4,Della,Conant,dconant3@japanpost.jp,Female,229.74.161.94 + 5,Sansone,Hardson,shardson4@weather.com,Male,51.154.224.38 + 6,Lloyd,Cromley,lcromley5@wikipedia.org,Male,168.145.20.63 + 7,Ty,Bamsey,tbamsey6@ca.gov,Male,129.204.46.174 + 8,Hurlee,Dumphy,hdumphy7@skyrock.com,Male,95.17.55.115 + 9,Andy,Vickarman,avickarman8@qq.com,Male,10.159.118.60 + 10,Jerad,Kerley,jkerley9@miitbeian.gov.cn,Male,3.19.136.57 + """; + + // Parse CSV into rows of fields with commented lines, those starting with pound or hash + // (#), removed. + + var rows = + from row in Regex.Split(csv.Trim(), "\r?\n") + select row.Trim() into row + where row.Length > 0 && row[0] != '#' + select row.Trim().Split(','); + + // Split header and data rows: + + var (header, data) = + rows.Index() + .Partition(e => e.Key == 0, + (hr, dr) => (hr.Single().Value, from e in dr select e.Value)); + + // Locate indices of headers: + + int[] bindings = [..from h in new[] { "id", "email", "last_name", "first_name", "foo" } + select Array.FindIndex(header, sh => sh == h)]; + + // Bind to data using indices: + + string? missing = null; + + var result = + from row in data + select row.BindByIndex(bindings, bindings.Length, _ => missing, (f, _) => f) + .Fold((id, email, ln, fn, foo) => + id is null || email is null || ln is null || fn is null + ? null + : new + { + Id = int.Parse(id, NumberStyles.None, CultureInfo.InvariantCulture), + FirstName = fn, + LastName = ln, + Email = email, + Foo = foo, + }); + + result.AssertSequenceEqual( + new { Id = 1 , FirstName = "Maggee" , LastName = "Hould" , Email = "mhould0@ft.com" , Foo = missing }, + new { Id = 2 , FirstName = "Judas" , LastName = "Vedekhov" , Email = "jvedekhov1@google.co.uk" , Foo = missing }, + new { Id = 3 , FirstName = "Sharity", LastName = "Desquesnes", Email = "sdesquesnes2@accuweather.com", Foo = missing }, + new { Id = 4 , FirstName = "Della" , LastName = "Conant" , Email = "dconant3@japanpost.jp" , Foo = missing }, + new { Id = 5 , FirstName = "Sansone", LastName = "Hardson" , Email = "shardson4@weather.com" , Foo = missing }, + new { Id = 6 , FirstName = "Lloyd" , LastName = "Cromley" , Email = "lcromley5@wikipedia.org" , Foo = missing }, + new { Id = 7 , FirstName = "Ty" , LastName = "Bamsey" , Email = "tbamsey6@ca.gov" , Foo = missing }, + new { Id = 8 , FirstName = "Hurlee" , LastName = "Dumphy" , Email = "hdumphy7@skyrock.com" , Foo = missing }, + new { Id = 9 , FirstName = "Andy" , LastName = "Vickarman" , Email = "avickarman8@qq.com" , Foo = missing }, + new { Id = 10, FirstName = "Jerad" , LastName = "Kerley" , Email = "jkerley9@miitbeian.gov.cn" , Foo = missing }); + } + } +}