Просидев на одном предприятии несколько лет, я решил поискать альтернативы. Специально не привожу детали по моей должности, квалификации и стажу, чтобы не создавать предвзятое впечатление и не влиять на объективность оценки выполнения тестового задания. По моему профилю вакансий оказалось довольно много. Откликнулся на первую попавшуюся вакансию очень близко к дому. Перезвонили в течении нескольких часов, обрисовали буквально в двух словах чем занимается контора (обмен данными между системами разных уровней) и предложили сделать тестовое задание. Выполнив задание примерно за сутки, я его отправил и через пару часов получил ответ: «задание Вы выполнили действительно отвратительно, халтурно» и отказ от дальнейших комментариев. По месту своей основной работы я много раз выполнял очень разные задания от очень разных людей, но такого ответа никогда не было даже близко. Что же тут произошло?
Поскольку я не принимал никаких обязательств по неразглашению, привожу задание полностью. Обратите внимание, никаких дополнительных сведений не предоставлено!
Во вложение класс C#, который предлагается реализовать. Описание методов - в xml-комментах. Обращаю Ваше внимание, что класс должен быть эффективным и не использовать много памяти и ресурсов даже тогда, когда в расписании задано много значений. Например очень много значений с шагом в одну миллисекунду.
Вложенный в задание файл schedule.cs
using System;
namespace Test
{
/// <summary>
/// Класс для задания и расчета времени по расписанию.
/// </summary>
public class Schedule
{
/// <summary>
/// Создает пустой экземпляр, который будет соответствовать
/// расписанию типа "*.*.* * *:*:*.*" (раз в 1 мс).
/// </summary>
public Schedule()
{
}
/// <summary>
/// Создает экземпляр из строки с представлением расписания.
/// </summary>
/// <param name="scheduleString">Строка расписания.
/// Формат строки:
/// yyyy.MM.dd w HH:mm:ss.fff
/// yyyy.MM.dd HH:mm:ss.fff
/// HH:mm:ss.fff
/// yyyy.MM.dd w HH:mm:ss
/// yyyy.MM.dd HH:mm:ss
/// HH:mm:ss
/// Где yyyy - год (2000-2100)
/// MM - месяц (1-12)
/// dd - число месяца (1-31 или 32). 32 означает последнее число месяца
/// w - день недели (0-6). 0 - воскресенье, 6 - суббота
/// HH - часы (0-23)
/// mm - минуты (0-59)
/// ss - секунды (0-59)
/// fff - миллисекунды (0-999). Если не указаны, то 0
/// Каждую часть даты/времени можно задавать в виде списков и диапазонов.
/// Например:
/// 1,2,3-5,10-20/3
/// означает список 1,2,3,4,5,10,13,16,19
/// Дробью задается шаг в списке.
/// Звездочка означает любое возможное значение.
/// Например (для часов):
/// */4
/// означает 0,4,8,12,16,20
/// Вместо списка чисел месяца можно указать 32. Это означает последнее
/// число любого месяца.
/// Пример:
/// *.9.*/2 1-5 10:00:00.000
/// означает 10:00 во все дни с пн. по пт. по нечетным числам в сентябре
/// *:00:00
/// означает начало любого часа
/// *.*.01 01:30:00
/// означает 01:30 по первым числам каждого месяца
/// </param>
public Schedule(string scheduleString)
{
}
/// <summary>
/// Возвращает следующий ближайший к заданному времени момент в расписании или
/// само заданное время, если оно есть в расписании.
/// </summary>
/// <param name="t1">Заданное время</param>
/// <returns>Ближайший момент времени в расписании</returns>
public DateTime NearestEvent(DateTime t1)
{
}
/// <summary>
/// Возвращает предыдущий ближайший к заданному времени момент в расписании или
/// само заданное время, если оно есть в расписании.
/// </summary>
/// <param name="t1">Заданное время</param>
/// <returns>Ближайший момент времени в расписании</returns>
public DateTime NearestPrevEvent(DateTime t1)
{
}
/// <summary>
/// Возвращает следующий момент времени в расписании.
/// </summary>
/// <param name="t1">Время, от которого нужно отступить</param>
/// <returns>Следующий момент времени в расписании</returns>
public DateTime NextEvent(DateTime t1)
{
}
/// <summary>
/// Возвращает предыдущий момент времени в расписании.
/// </summary>
/// <param name="t1">Время, от которого нужно отступить</param>
/// <returns>Предыдущий момент времени в расписании</returns>
public DateTime PrevEvent(DateTime t1)
{
}
}
}
Если коротко, то предлагается реализовать парсинг строки, определяющей расписание событий, а также пару методов получения времени события, ближайшего к указанному времени.
Меня сразу насторожило неконкретное требование «класс должен быть эффективным и не использовать много памяти и ресурсов», ведь понятия «эффективно» и «много» каждый понимает по-своему. Чтобы грубо не нарушать эти требования, я решил сразу отметать плохо зарекомендовавшие себя в плане эффективности практики типа регулярных выражений и частого выделения объектов в «куче» (heap) чтобы не нагружать сборщик мусора. А также предусмотреть потенциальные пути оптимизации на случай если нужно будет улучшать быстродействие или уменьшать выделяемую память. Добиваться каких то экстремальных показателей в плане оптимизации нет смысла, потому что это приведёт к снижению такого важного показателя как поддерживаемость кода, а будет ли от этого польза — непонятно, поскольку неизвестны условия эксплуатации. На случай будущего сравнения разных оптимизаций, сразу добавил в проект бенчмарки.
Главное, на чём я решил сосредоточиться при выполнении задания — аккуратность обращения с календарём. Ведь, как известно, наш Григорианский календарь является нерегулярным. Все знают, что не каждый год содержит 365 дней и не каждый месяц содержит 31 день. В дополнение к этому, не каждая минута содержит 60 секунд. Не говоря уже о введениях/отменах перехода на зимнее время. Поэтому сразу было решено отказаться от арифметических операций с временами и датами и использовать для этого только библиотечные методы в классах DateTime или DateTimeOffset.
Первым делом написал модульные тесты используя примеры, указанные заказчиком. Также добавил от себя несколько тестов по граничным значениям. Хотя сделать тесты мог бы и сам заказчик для экономии времени на тестирование кандидатов.
Перебирая возможные способы реализации, понял, что это можно делать очень долго. Учитывая объём функциональности класса в сравнении с объёмом моих типичных проектов, решил ограничить себя одним рабочим днём. В результате появилось приемлемое решение, которое не является ни экстремально плохим, ни экстремально хорошим по эффективности. Зато легко для понимая кода и содержит простор для дальнейшей оптимизации. Для всех имеющихся циклов было оценено количество максимально возможных итераций, а также количество итераций при типичном использовании. Выделение памяти из «кучи» присутствует только при создании объекта. В методах создаются только объекты-значения, которые располагаются в стэке и бесследно исчезают при завершении метода.
Моё решение размещено на гитхабе в виде проекта Visual Studio. Я не понимаю, почему я получил оценку «отвратительно, халтурно»! И неужели сейчас принято так оценивать задания: не говорить в чём проблема, не давать направлений для дальнейшего совершенствования специалиста? Я показал проект уважаемому коллеге, он указал только на те недостатки, которые я и сам вижу и это не объясняет низкой оценки. Уважаемые специалисты, объясните, что не так с моим тестовым заданием? Обещаю, что дополню статью выявленной информацией.