Kusek
Kusek

Reputation: 5384

LINQ "zip" in String Array

Say there are two arrays:

String[] title = { "One","Two","three","Four"};
String[] user = { "rob","","john",""};

I need to filter out the above array when the user value is Empty then join or zip the two together. Final Output should be like:

{ "One:rob", "three:john" } 

How can this be done using LINQ?

Upvotes: 12

Views: 10988

Answers (6)

Robert P
Robert P

Reputation: 15968

As of .net Core 3.0 and up, the Zip extension method is now built in.

int[] numbers = { 1, 2, 3, 4 };
string[] words = { "one", "two", "three" };

var numbersAndWords = numbers.Zip(words, (first, second) => first + " " + second);

foreach (var item in numbersAndWords)
    Console.WriteLine(item);
// This code produces the following output:

// 1 one
// 2 two
// 3 three

See more examples at https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.zip?view=net-5.0

Upvotes: 1

ΩmegaMan
ΩmegaMan

Reputation: 31596

In looking at Marc's answer (and ultimately the Zip method as of .Net 4), there is a significant amount of overhead to enumerate and join of rows where they are eventually thrown away; can that be done without that waste?

In looking at Jon's answer, creating a projection of dynamic entities to reference the existing data and then creating a new set of entities from that mirror could be a deterrent to using that method if the total row count were overly large.

The below snippet uses the references to the original data and the only wasted projected entitles created are the ones which have null string which are subsequently removed. Also the enumeration of the data is kept to a minimum.

String[] title = { "One","Two","three","Four"};
String[] user  = { "rob","","john",""};

user.Select ((usr, index) => string.IsNullOrEmpty(usr) 
                             ? string.Empty 
                             : string.Format("{0}:{1}", title[index], usr ))
    .Where (cmb => string.IsNullOrEmpty(cmb) == false)

As an aside, this methodology could have user array which is smaller in size than the title array as a plus.


The Aggregate function is overlooked, here it is in action:

int index = 0;
user.Aggregate (new List<string>(), 
                (result, usr) => 
                     {  
                        if (string.IsNullOrEmpty(usr) == false)
                           result.Add(string.Format("{0}:{1}", title[index], usr));
                        ++index;
                        return result;
                      } )

Upvotes: 0

Daniel Earwicker
Daniel Earwicker

Reputation: 116654

As another addition to the previous answers, Zip is usually defined and used in conjunction with a Tuple type. This relieves the user of having to provide a resultSelector function.

public class Tuple<TItem1, TItem2> // other definitions for higher arity
{
    public TItem1 Item1 { get; private set; }
    public TItem2 Item2 { get; private set; }

    public Tuple(TItem1 item1, TItem2 item2)
    {
        Item1 = item1;
        Item2 = item2;
    }
}

And hence:

public static IEnumerable<Tuple<TFirst, TSecond>> Zip<TFirst, TSecond>
    (this IEnumerable<TFirst> first, IEnumerable<TSecond> second) 
{
    using (IEnumerator<TFirst> e1 = first.GetEnumerator())
    using (IEnumerator<TSecond> e2 = second.GetEnumerator())
    {
        while (e1.MoveNext() && e2.MoveNext())
            yield return new Tuple<TFirst, TSecond>(e1.Current, e2.Current);
    }
}

I believe this is closer to what CLR 4.0 will have (though it may have the more flexible variety as well).

Upvotes: 0

driis
driis

Reputation: 164281

As a complement to the already posted answers, here is a solution without using the Zip method. This assumes that both arrays is of same length.

        var pairs = from idx in Enumerable.Range(0, title.Length)
                    let pair = new {Title = title[idx], User = user[idx]}
                    where !String.IsNullOrEmpty(pair.User)
                    select String.Format("{0}:{1}", pair.Title, pair.User);

Upvotes: 5

Jon Skeet
Jon Skeet

Reputation: 1499940

For a start, you need a Zip operator to zip the two arrays together. Here's an abbreviated version of the code from Eric Lippert's blog (no error checking in this version, just for brevity):

public static IEnumerable<TResult> Zip<TFirst, TSecond, TResult>
    (this IEnumerable<TFirst> first, 
    IEnumerable<TSecond> second, 
    Func<TFirst, TSecond, TResult> resultSelector) 
{
    using (IEnumerator<TFirst> e1 = first.GetEnumerator())
        using (IEnumerator<TSecond> e2 = second.GetEnumerator())
            while (e1.MoveNext() && e2.MoveNext())
                yield return resultSelector(e1.Current, e2.Current);
}

Note that Zip will be in the standard libraries for .NET 4.0.

Then you need to just apply a filter and a projection. So we'd get:

var results = title.Zip(user, (Title, User) => new { Title, User })
                   .Where(x => x.Title != "")
                   .Select(x => x.Title + ":" + x.User);

Upvotes: 10

Marc Gravell
Marc Gravell

Reputation: 1062580

It sounds like you actually want to "zip" the data together (not join) - i.e. match pairwise; is that correct? If so, simply:

    var qry = from row in title.Zip(user, (t, u) => new { Title = t, User = u })
              where !string.IsNullOrEmpty(row.User)
              select row.Title + ":" + row.User;
    foreach (string s in qry) Console.WriteLine(s);

using the Zip operation from here:

// http://blogs.msdn.com/ericlippert/archive/2009/05/07/zip-me-up.aspx
public static IEnumerable<TResult> Zip<TFirst, TSecond, TResult>
(this IEnumerable<TFirst> first,
IEnumerable<TSecond> second,
Func<TFirst, TSecond, TResult> resultSelector)
{
    if (first == null) throw new ArgumentNullException("first");
    if (second == null) throw new ArgumentNullException("second");
    if (resultSelector == null) throw new ArgumentNullException("resultSelector");
    return ZipIterator(first, second, resultSelector);
}

private static IEnumerable<TResult> ZipIterator<TFirst, TSecond, TResult>
    (IEnumerable<TFirst> first,
    IEnumerable<TSecond> second,
    Func<TFirst, TSecond, TResult> resultSelector)
{
    using (IEnumerator<TFirst> e1 = first.GetEnumerator())
    using (IEnumerator<TSecond> e2 = second.GetEnumerator())
        while (e1.MoveNext() && e2.MoveNext())
            yield return resultSelector(e1.Current, e2.Current);
}

Upvotes: 8

Related Questions