Разделение списка на алфавитные диапазоны

#c# #list

#c# #Список

Вопрос:

Мне нужно разделить список на несколько списков, которые сгруппированы в соответствии с первым символом строкового свойства. Вот пример.

 class Program
{
    static void Main(string[] args)
    {
        var makes = new List<VehicleMake>
                        {
                            new VehicleMake {Name = "Acura"},
                            new VehicleMake {Name = "AMG"},
                            new VehicleMake {Name = "Audi"},
                            new VehicleMake {Name = "BMW"},
                            new VehicleMake {Name = "Chevrolet"},
                            new VehicleMake {Name = "Datsun"},
                            new VehicleMake {Name = "Eagle"},
                            new VehicleMake {Name = "Fiat"},
                            new VehicleMake {Name = "Honda"},
                            new VehicleMake {Name = "Infiniti"},
                            new VehicleMake {Name = "Jaguar"}
                        };

        var balancedLists = makes.Balance(new List<BalancedListGroup>
                          {
                              new BalancedListGroup { RangeStart = 'A', RangeEnd = 'C'},
                              new BalancedListGroup { RangeStart = 'D', RangeEnd = 'F'},
                              new BalancedListGroup { RangeStart = 'G', RangeEnd = 'J'},
                          });

        foreach (var balancedList in balancedLists)
        {
            foreach (var vehicleMake in balancedList)
            {
                Console.WriteLine(vehicleMake.Name);
            }
            Console.WriteLine("---");
        }
        Console.ReadLine();
    }
}

public class VehicleMake
{
    public string Name { get; set; }
}

public static class VehicleMakeListBalancer
{
    public static List<List<VehicleMake>> Balance(this List<VehicleMake> list, List<BalancedListGroup> groups)
    {
        var letters =
            new List<string> { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "y", "z" };
        var balancedLists = new List<List<VehicleMake>>();
        foreach (var group in groups)
        {
            var groupList = new List<VehicleMake>();
            for (var i = letters.IndexOf(group.RangeStart.ToString().ToLower()); i <= letters.IndexOf(group.RangeEnd.ToString().ToLower()); i  )
            {
                groupList.AddRange(list.Where(l => l.Name.ToLower().StartsWith(letters[i].ToString())).ToList());
            }
            balancedLists.Add(groupList);
        }

        return balancedLists;
    }
}

public class BalancedListGroup
{
    public char RangeStart { get; set; }
    public char RangeEnd { get; set; }
}
  

Который выводит:

 Acura
AMG
Audi
BMW
Chevrolet
---
Datsun
Eagle
Fiat
---
Honda
Infiniti
Jaguar
---
  

Этот алгоритм работает, но кажется очень неуклюжим. Есть ли более элегантный способ сделать это?

Ответ №1:

Следующий метод расширения использует linq для выбора всех марок транспортных средств, название которых начинается с диапазона символов.

         public static List<VehicleMake> GetInRange(this List<VehicleMake> vehicleList, char RangeStart, char RangeEnd)
        {
            var vehiclesInRange = from vm in vehicleList
                                  where vm.Name[0] >= RangeStart amp;amp; vm.Name[0] <= RangeEnd
                                  select vm;

            return vehiclesInRange.ToList();
        }
  

ПРИМЕР ИСПОЛЬЗОВАНИЯ

     static class Program
    {
        static void Main(string[] args)
        {
            var makes = new List<VehicleMake> { 
                new VehicleMake { Name = "Acura" },
                new VehicleMake { Name = "AMG" },
                new VehicleMake { Name = "Audi" }, 
                new VehicleMake { Name = "BMW" }, 
                new VehicleMake { Name = "Chevrolet" },
                new VehicleMake { Name = "Datsun" },
                new VehicleMake { Name = "Eagle" }, 
                new VehicleMake { Name = "Fiat" },
                new VehicleMake { Name = "Honda" }, 
                new VehicleMake { Name = "Infiniti" },
                new VehicleMake { Name = "Jaguar" } 
            }; 


            var atoc =  makes.GetInRange('A', 'C');
            atoc.Print();

            var dtom = makes.GetInRange('D', 'M');
            dtom.Print();

            var mtoz = makes.GetInRange('M', 'Z');
            mtoz.Print();

            Console.ReadLine();
        }

        static List<VehicleMake> GetInRange(this List<VehicleMake> vehicleList, char RangeStart, char RangeEnd)
        {
            var vehiclesInRange = from vm in vehicleList
                                  where vm.Name[0] >= RangeStart amp;amp; vm.Name[0] <= RangeEnd
                                  select vm;

            return vehiclesInRange.ToList();
        }

        static void Print(this List<VehicleMake> vehicles)
        {
            Console.WriteLine();
            vehicles.ForEach(v => Console.WriteLine(v.Name));
        }
    }
  

Комментарии:

1. Тьфу, я был так близок к этому, когда начал писать этот алгоритм, за исключением того, что я пытался сделать это для всей строки, а не только для первого символа. Хотя это идеально. Спасибо!

Ответ №2:

Вы можете использовать GroupBy() для достижения желаемого — сгруппировать по первой букве вашего транспортного средства, затем составить из них подсписки:

 var balancedLists = makes.GroupBy(x => x.Name[0]).Select( x=> x.ToList())
                         .ToList();
  

Однако при этом будут созданы группы, каждая из которых содержит только одну букву — чтобы изменить поведение группировки, вы могли бы предоставить пользовательский метод GetGroup( char c) , который возвращает целое число для идентификации группы.

В качестве альтернативы, если вам нужны только сбалансированные группы, вы могли бы использовать индекс для группировки транспортных средств в группы одинакового размера:

 var balancedLists = makes.Select((vehicle, index) => new { Index = index, Vehicle = vehicle })
                    .GroupBy(x => x.Index / 3)
                    .Select(g => g.Select(x => x.Vehicle).ToList())
                    .ToList();
  

Комментарии:

1. Спасибо, но в этом примере группируется только по первой букве. Мне нужно, чтобы группы были ранжированы. В моем примере я ожидаю увидеть 3 списка. Один со всеми начинается с A-C, один со всеми начинается с D-F, и один со всеми начинается с G-J. Строки с тремя дефисами в примере вывода ограничивают каждый список, который выводится моим алгоритмом.

Ответ №3:

Если бы у вас был объект со следующими инвариантами:

  • Может содержать несколько элементов balancedlistgroup
  • Для данного переданного транспортного средства возвращает, например, целое число, одинаковое для любого транспортного средства, попадающего в заданный диапазон

Затем вы можете сгруппировать свой список транспортных средств с помощью этого объекта:

 var groups = vehicles.GroupBy(x => rangeContainer.GroupKey(x))
  

Ответ №4:

Другой вариант Linq, если хотите.

         var makes = new List<VehicleMake> {
            new VehicleMake { Name = "Acura" }, 
            new VehicleMake { Name = "AMG" }, 
            new VehicleMake { Name = "Audi" }, 
            new VehicleMake { Name = "BMW" }, 
            new VehicleMake { Name = "Chevrolet" }, 
            new VehicleMake { Name = "Datsun" }, 
            new VehicleMake { Name = "Eagle" }, 
            new VehicleMake { Name = "Fiat" }, 
            new VehicleMake { Name = "Honda" }, 
            new VehicleMake { Name = "Infiniti" }, 
            new VehicleMake { Name = "Jaguar" } };


        var balancedLists = new List<BalancedListGroup> 
        { 
            new BalancedListGroup { RangeStart = 'A', RangeEnd = 'C' }, 
            new BalancedListGroup { RangeStart = 'D', RangeEnd = 'F' }, 
            new BalancedListGroup { RangeStart = 'G', RangeEnd = 'J' }, 
        };

        List<List<VehicleMake>> brandedMakes = new List<List<VehicleMake>>();
        foreach (var x in balancedLists)
        {
            brandedMakes.Add(makes.Where(a => a.Name.Substring(0, 1)[0] >= x.RangeStart amp;amp; a.Name.Substring(0, 1)[0] < x.RangeEnd).ToList());
        }
  

Ответ №5:

Я считаю, что этот запрос эффективно выполнит то, что вам нужно:

 var letterGroupTuples 
    = from blGroup in groups
      from letter in Enumerable.Range
                 (blGroup.RangeStart, blGroup.RangeEnd - blGroup.RangeStart   1)
      select new { Letter = char.ToLower((char)letter), BlGroup = blGroup };

var groupsForLetters = letterGroupTuples.ToDictionary
                       (a => a.Letter, a => a.BlGroup);

var query = from vehicleMake in list
            let key = vehicleMake.Name.ToLower().First()
            where groupsForLetters.ContainsKey(key)
            group vehicleMake by groupsForLetters[key] into bucket
            select bucket.ToList();

return query.ToList();
  

Идея состоит в том, чтобы:

  1. Создайте хэш-таблицу из допустимых букв в соответствующую корзину.
  2. Группируйте элементы в нужную корзину, используя хэш-таблицу, отфильтровывая те элементы, у которых нет соответствующей корзины.

Ответ №6:

Я добавлю свои 2p в:

Я начал с того, что предположил очень плохую вещь, чтобы упростить свой код. А именно, что вы работаете с символами ascii верхнего регистра для ваших начальных букв. Очевидно, что мой код может быть изменен для работы с вашими структурами диапазонов.

Итак, я получаю свои начальные диапазоны следующим образом:

 var initialGroups = new List<IEnumerable<char>> 
                  { 
                    Enumerable.Range((int)'A', 3).Select(i => (char)i)
                  , Enumerable.Range((int)'D', 3).Select(i => (char)i)
                  , Enumerable.Range((int)'G', 4).Select(i => (char)i) 
                  };
  

И метод получения групп таков:

 IEnumerable<IEnumerable<string>> GroupByInitial(List<string> cars, List<IEnumerable<char>> initialGroups)
{
    var groups = from grp in initialGroups
                 from car in cars 
                 where grp.Contains(car[0])
                 select new {grp, car};
    return groups.GroupBy(group => group.grp).Select(group => group.Select(grouping => grouping.car));
}