Brendan Long
Brendan Long

Reputation: 54242

Find and map at the same time

I have a data structure where Modules contain Units and Units contain Sections, and from a list of modules, I want to find the first module that contains at least one unit that contains at least one section, and I want to do something with the module, unit and section.

I initially tried to use modules.Find() for this, but it only tells me what the first non-empty Module is, so I'd have to lookup the Unit twice:

var module = modules.Find(m => m.Units.Exists(u => u.Sections.Count > 0));
if (module == null)
{
  throw new Exception("there are no non-empty modules");
}
var unit = module.Units.Find(u => u.Sections.Count > 0);
var section = unit.Sections.First();
doSomeStuff(module, unit, section);

I eventually wrote my own function to do this:

private Tuple<Module, Unit, Section> getFirstModuleWithVisibleSection(List<Module> modules)
{
    foreach (var module in modules)
    {
        foreach (var unit in module.Units)
        {
            var section = unit.Sections.FirstOrDefault();
            if (section != null)
            {
                return new Tuple<Module, Unit, Section>(module, unit, section);
            }
        }
    }
    return null;
}

...

var res = getFirstModuleWithVisibleSection(modules);
if (res == null)
{
    throw new Exception("no visible modules");
}
var module = res.Item1;
var unit = res.Item2;
var section = res.Item3;
doSomething(module, unit, section);

This is efficient but it's way more verbose than I was hoping for.

I'm more used to OCaml, where I would use List.find_map, which is like find, except instead of returning true/false you return null or not-null, and it returns the first not-null. In C# it would look something like this:

var (module, unit, section) =
  modules.FindMap(module =>
    module.Units.FindMap(unit =>
    {
      var section = unit.Sections.FirstOrDefault();
      if (section == null)
      {
        return null;
      }
      return (module, unit, section);
    }));

Is there a way to do this in C#?

Upvotes: 1

Views: 122

Answers (2)

Silvermind
Silvermind

Reputation: 5944

What about:

var query = from m in modules
            from u in m.Units
            let s = u.Sections.FirstOrDefault()
            where s != null
            select new
            {
                m,
                u,
                s
            };
var item = query.FirstOrDefault();

Upvotes: 4

JSteward
JSteward

Reputation: 7091

Certainly not elegant but it may meet the need.

public Module FirstModuleWithAUnitWithASection(IEnumerable<Module> modules)
    => modules.Where(module => module.Units != null)
    .Select(module => module.Units.Where(unit => unit.Sections != null)
    .Select(unit => unit.Sections.Select(section => module)
    .First()).First()).First();

Upvotes: 1

Related Questions