Помогите улучшить код на C#

Подскажите как оптимизировать код на C#:
есть строка такого вида:

График работы
Пн
08:30 – 20:00
Вт
08:30 – 20:00
Ср
08:30 – 20:00
Чт
08:30 – 20:00
Пт
08:30 – 20:00
Сб
08:30 – 17:00
Вс
Выходной

нужно сделать строку в краткой записи:

Пн-Пт 08:30 – 20:00
Сб 08:30 – 17:00
Вс Выходной

вот мой код:

static void Main(string[] args)
{
    
    #region График работы
    string graf = @"График работы
                    Пн
                    10:00 – 21:00
                    Вт
                    10:00 – 21:00
                    Ср
                    10:00 – 21:00
                    Чт
                    10:00 – 21:00
                    Пт
                    10:00 – 18:00
                    Сб
                    Выходной
                    Вс
                    Выходной";
    #endregion

    //Создаем массив из строковой переменной
    string[] arrGraf = graf.Split('\n').Where(x => x.Trim() != "").ToArray();

    //Создаем спсисок для храния графика в виде: Пн 10:00 - 21:00
    List<string> listGrafic = new List<string>();

    int a = 0;        //счетчик
    string day = "";  //день недели
    string time = ""; //рабочее время
    string rgx = "";  //данные от регулярки

    //перебираем массив
    while (a < arrGraf.Length)
    {

        string data = arrGraf[a].Trim();

        switch (data.ToLower().Trim())
        {
            case "пн":
                day = data.Trim();
                break;
            case "вт":
                day = data.Trim();
                break;
            case "ср":
                day = data.Trim();
                break;
            case "чт":
                day = data.Trim();
                break;
            case "пт":
                day = data.Trim();
                break;
            case "сб":
                day = data.Trim();
                break;
            case "вс":
                day = data.Trim();
                break;

            default:
                rgx = new Regex("^\\d+").Match(data.Trim()).ToString();
                time = rgx == "" ? "" : data.Trim();
                break;

        }

        if (day != "" && time != "")
        {
            string dayTime = day + " " + time;
            listGrafic.Add(dayTime); //добавляем в список
            day = ""; time = ""; //обнуляем

        }

        a++;//счетчик
    }


    Console.WriteLine(String.Join("\n", listGrafic));

    Console.WriteLine();
    Console.WriteLine();


    string res = "";

    //перебор массива с графиком
    for (int i = 0; i < listGrafic.Count; i++)
    {

        
       string temp1 = Regex.Replace(listGrafic[i].Trim().ToLower(), @"\D+", "").Trim();
       string temp2 = "";
       if (i + 1 < listGrafic.Count)
       {
            temp2 = Regex.Replace(listGrafic[i + 1].Trim().ToLower(), @"\D+", "").Trim();
       }
       
        res += temp1 == temp2 ? listGrafic[i] + "|" : listGrafic[i] + Environment.NewLine;
        
        continue;

    }

    StringBuilder sb = new StringBuilder();
    string result = "";
    //делаем перебор полученного результата
    foreach (var rowGraf in res.Trim('|').Split('\n'))
    {
        if (rowGraf.Trim() == "") continue;

        //получаем сторку вида: Пн 10:00 - 21:00|Вт 10:00 - 21:00|Ср 10:00 - 21:00|
        if (rowGraf.Length > 1)
        {
            
            string[] arr = rowGraf.Split(new string[] { "|", "\r" }, StringSplitOptions.RemoveEmptyEntries);
            if (arr.Length == 1)
            {
                result = arr[0].Trim('|') ;
                sb.AppendLine(result);
                continue;

            }
            result = arr[0].Trim('|');
            string replase = result.Substring(2);
            string lastRow = arr.Last().Trim('|');
            result = result.Replace(replase, "-" + lastRow);
            sb.AppendLine(result);
            

        }

    }

    Console.WriteLine(sb.ToString().Trim());
    Console.ReadKey();
}

Ответы (2 шт):

Автор решения: EvgeniyZ

Смотрите как можно сделать...

  • Делаем класс, который будет содержать в себе нужную информацию, а именно начало, конец и день недели.

    public class JobTime
    {
        public JobTime(string dayOfWeek, TimeSpan begin, TimeSpan end)
            => (DayOfWeek, Begin, End) = (dayOfWeek, begin, end);
        public string DayOfWeek { get; }
        public TimeSpan Begin { get; }
        public TimeSpan End { get; }
    }
    

    Время тут в понятном формате - TimeSpan. Неделя же, ее лучше сделать тоже нужным форматом (DayOfWeek), но тут возникнет ряд проблем с конвертацией и отображением, по этому я решил для примера оставить в виде string.

  • Далее давайте сделаем метод, который вернет нам заполненный массив этого класса, разобрав входную строку:

    private static List<JobTime> ParseData(string text)
    {
        var lines = text.Split(Environment.NewLine).Skip(1).ToArray();
        var result = new List<JobTime>();
    
        for (int i = 1; i < lines.Length; i += 2)
        {
            var dayOfWeak = lines[i - 1];
            TimeSpan begin = default;
            TimeSpan end = default;
    
            var time = lines[i].Split('–');
            if (time.Length == 2)
            {
                TimeSpan.TryParse(time[0], out begin);
                TimeSpan.TryParse(time[1], out end);
            }
    
            result.Add(new JobTime(dayOfWeak, begin, end));
        }
    
        return result;
    }
    

    Логика метода думаю вам понятна, если в двух словах, то:

    1. Разбиваем входной текст построчно (Environment.NewLine - символ новой строки).
    2. При помощи LINQ берем из полученного массива строк все, кроме первой (это "График работы").
    3. Преобразуем в массив, что бы можно было обратиться без проблем по индексу к нужному объекту.
    4. Далее делаем цикл, который будет выдавать нам значения с шагом 2, начиная с 1 (1, 3, 5 и тд.).
    5. В этом цикле берем значения из массива, где i -1 будет днем недели, а просто i будет временем.
    6. Полученное время разбиваем по нужному символу
    7. Если получился массив из 2-х объектов, то конвертируем каждое в TimeSpan.
    8. Создаем объект нашего класса и добавляем в коллекцию.
  • Собственно это уже больше половины дела, осталось просто сформировать данные так, как нам нужно. Если посмотреть на ваши выходные данные, то можно назвать это некими группами, так давайте это сгруппируем:

    var data = ParseData(str);
    var group = data.GroupBy(x => new { x.Begin, x.End })
        .Select(x =>
        {
            var times = x.Key.Begin == default ? "Выходной" : $"{x.Key.Begin:hh\\:mm} - {x.Key.End:hh\\:mm}";
            var days = x.First() == x.Last() ? x.First().DayOfWeek : $"{x.First().DayOfWeek}-{x.Last().DayOfWeek}";
            return $"{days} {times}";
        });
    
    1. При помощи GroupBy() группируем все по двух значениям (начало и конец).
    2. Select() поможет сформировать нужный вид.
    3. x.Key.Begin == default - если начальное время будет равно 00:00:00, то можем считать этот день выходным, иначе формируем текст в виде "времяНачала - времяКонца" убирая секунды.
    4. x.First() == x.Last() (можно и кол-во объектов в x проверить) - если первый и последний объект группы одинаковы, то выводим только день недели от первого, иначе "первый-последний".

Собственно вот и вся ваша задача в паре строк кода. Он не идеален, ибо если будет время идти в разнобой (а не как у вас по порядку), то выдаст кашу, решить которую можно простым циклом, а не группировкой, который будет проверять, подряд-ли идут дни или нет. Моя задача была лишь показать саму суть, что надо все преобразовать в нужные типы, сделать класс, хранящий в себе это все и уже дальше выводить как надо. Не перебирайте строки до посинения, это не правильно!

Онлайн пример: https://dotnetfiddle.net/XchIJM

→ Ссылка
Автор решения: Kir_Antipov

Жутко соскучился по код-гольфу, так что вот вам LINQ-однострочник:

string.Join(Environment.NewLine, (graf.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) is { } arr ? arr : arr = null).Where((x, i) => i % 2 == 1).Zip(arr.Skip(1).Where((x, i) => i % 2 == 1))
    .Select(pair => (Day: pair.First.Trim(), From: (new Regex(@"(\d\d:\d\d)\s*[-–]\s*(\d\d:\d\d)").Match(pair.Second).Groups is { Count: 3 } groups ? groups : groups = null)?[1].Value, To: groups?[2].Value))
    .Select(x => (x.Day, From: x.From is null ? default(TimeSpan?) : TimeSpan.Parse(x.From), To: x.To is null ? default(TimeSpan?) : TimeSpan.Parse(x.To)))
    .Aggregate(new List<(List<string> Days, TimeSpan? From, TimeSpan? To)>(7), (acc, day) => { if (acc.Count == 0 || (acc[^1].From, acc[^1].To) != (day.From, day.To)) { acc.Add((new List<string> { day.Day }, day.From, day.To)); } else { acc[^1].Days.Add(day.Day); } return acc; })
    .Select(x => (x.Days.Count == 0 ? x.Days[0] : $"{x.Days[0]}-{x.Days[^1]}") + " " + (x.From is null ? "Выходной" : $@"{x.From:hh\:mm} – {x.To:hh\:mm}")));

Для

string graf = @"График работы
        Пн
        10:00 – 21:00
        Вт
        10:00 – 21:00
        Ср
        10:00 – 21:00
        Чт
        10:00 – 21:00
        Пт
        10:00 – 18:00
        Сб
        Выходной
        Вс
        Выходной";

Отдает:

Пн-Чт 10:00 – 21:00
Пт 10:00 – 18:00
Сб-Вс Выходной


Чуть более развернуто и с пояснениями:

// Делим входную строку по символам конца строки, удаляя пустые вхождения
string[] lines = graf.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);

// Это регулярное выражение извлекает из строк вида "10:00 – 21:00" значения
// "10:00" и "21:00"
Regex timeExtractor = new Regex(@"(\d\d:\d\d)\s*[-–]\s*(\d\d:\d\d)", RegexOptions.Compiled);

IEnumerable<string> entries =           
    // Берем каждую вторую строку (Пн, Вт, Ср и т.д.),
    // и объединяем с каждой второй сточкой, но уже после первой линии
    // (10:00 – 21:00 и далее).
    // Таким образом в паре окажутся дни с их графиком работы:
    // (Пн, 10:00 – 21:00), (Вт, 10:00 – 21:00) и т.д.
    lines.Where((_, index) => index % 2 == 1).Zip(lines.Skip(1).Where((_, index) => index % 2 == 1))

    // Преобразуем пару (день, график) в кортеж (день, от, до)
    .Select(pair =>
    {
        // Если регулярное выражение отработало успешно,
        // то в первой группе будет время начала работы,
        // а во второй - время окончания рабочего дня
        Match match = timeExtractor.Match(pair.Second);
        if (match.Success)
            return (Day: pair.First.Trim(), From: TimeSpan.Parse(match.Groups[1].Value), To: TimeSpan.Parse(match.Groups[2].Value));
        else
            return (Day: pair.First.Trim(), From: default(TimeSpan?), To: default(TimeSpan?));
    })

    // Теперь же сожмем список, объединив дни, которые стоят рядом и имеют общий график работы
    .Aggregate(new List<(List<string> Days, TimeSpan? From, TimeSpan? To)>(7), (list, info) =>
    {
        // Если в списке ничего нет, либо же график последней записи отличается -
        // создать новую запись
        if (list.Count == 0 || (list[^1].From, list[^1].To) != (info.From, info.To))
        {
            list.Add((new List<string> { info.Day }, info.From, info.To));
        }
        // Иначе же добавить текущий день в предыдущую запись
        else
        {
            list[^1].Days.Add(info.Day);
        }

        return list;
    })

    // Ну а тут уже все просто:
    // лист дней ужимаем до "ПервыйДень-ПоследнийДень"/"ЕдинственныйДень",
    // а время работы, если такое имеется, приводим к строке, иначе отдаем "Выходной"
    .Select(x => (Days: x.Days.Count == 1 ? x.Days[0] : $"{x.Days[0]}-{x.Days[^1]}", Time: x.From is null ? "Выходной" : $@"{x.From:hh\:mm} – {x.To:hh\:mm}"))
                
    // После чего две полученные строки совмещаем
    // Можно было сделать и в предудыщем методе, но так нагляднее
    .Select(x => $"{x.Days} {x.Time}");

// Полученную выше коллекцию строк объединяем,
// разделяя вхождения новой строкой
string result = string.Join(Environment.NewLine, entries);
→ Ссылка