Просидев на одном предприятии несколько лет, я решил поискать альтернативы. Специально не привожу детали по моей должности, квалификации и стажу, чтобы не создавать предвзятое впечатление и не влиять на объективность оценки выполнения тестового задания. По моему профилю вакансий оказалось довольно много. Откликнулся на первую попавшуюся вакансию очень близко к дому. Перезвонили в течении нескольких часов, обрисовали буквально в двух словах чем занимается контора (обмен данными между системами разных уровней) и предложили сделать тестовое задание. Выполнив задание примерно за сутки, я его отправил и через пару часов получил ответ: «задание Вы выполнили действительно отвратительно, халтурно» и отказ от дальнейших комментариев. По месту своей основной работы я много раз выполнял очень разные задания от очень разных людей, но такого ответа никогда не было даже близко. Что же тут произошло?

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

Во вложение класс 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. Я не понимаю, почему я получил оценку «отвратительно, халтурно»! И неужели сейчас принято так оценивать задания: не говорить в чём проблема, не давать направлений для дальнейшего совершенствования специалиста? Я показал проект уважаемому коллеге, он указал только на те недостатки, которые я и сам вижу и это не объясняет низкой оценки. Уважаемые специалисты, объясните, что не так с моим тестовым заданием?

Огромное спасибо всем комментаторам, вы прояснили многое. Поскольку многие указали, что надо было уточнять детали у заказчика, поясняю: я пытался, но меня принципиально не хотели соединять со специалистами, со мной общался только HR.

Суммирую, что произошло с моим тестовым заданием. Априори исключаю обман и некомпетентность самого заказчика, это отдельная тема.

  1. У меня выявилась определённая проф.деформация. Я годами работал в исследовательском подразделении, где код надо писать быстро, потом использовать его в реальных условиях у заказчика, а потом по результатам использования он может вообще не понадобится. Переход в продакшн использовался рудиментарный. Заказчик относился лояльно и даже порой требовал, чтобы как то заработало побыстрее, хоть и с недочётами. Код поддерживал только один человек — его автор. Это не значит, что я писал плохой код. Но, конечно, к некоторым из правил, применяемых в серьёзных фирмах по разработке софта, я просто не стремился.

  2. Я неверно оценил чего от меня ожидает заказчик. Я оценивал буквально, по фразам в задании. Соответственно, предположил, что в приоритете — высокая оптимизация по скорости и памяти. Как заметили тут в комментариях, скорее всего ожидалось лишь «избегать лишних созданий коллекций, ставить Capacity по возможности и не конкатенировать строки в цикле».

  3. Я недостаточно обосновал принятые решения, такие как отказ от регулярок и парсинг числа кустарным методом. Собственно, для меня образцом всегда был и остаётся код самого .Net. И такие решения я смотрю именно там. Наверное, надо было написать в комментариях побольше обоснований и возможных альтернатив.

  4. Основная претензия к моему решению. «Код плохо читаемый из за множества вложенных If, я такое тоже не люблю и на проектах стараюсь избегать если можно без них». Я не думал, что количество вложений if считается таким уж страшным злом если строки короткие и понятные. Я добивался цели удобно увидеть алгоритм целиком на одном экране. Для сравнения можете посмотреть метод GetTimeAfter(), который делает примерно тоже самое в библиотеке Quartz. У них максимально 7 отступов, у меня — 11 потому что добавляются доли секунды. Но их код размазан на много экранов и понять алгоритм очень трудно.

  5. Единственный однозначный мой недочёт — не объединил два похожих метода в один с дополнительным параметром.

Only registered users can participate in poll. Log in, please.
Версии неожиданной оценки:
21.36%Заказчику хотелось получить красиво выглядящий в IDE код и тонну комментариев, по объёму втрое больше кода.66
15.53%Не хватило обоснования выбранных решений. Непонятно почему кто-то выделил дополнительную память или не применил регулярные выражения.48
33.33%Тестовое задание ни о чем. Видимо ищут джуна, чтобы валить на него косяки и пинать его без повода.103
52.43%Нужен был код в production, а делать не хотели. И кинули, как тестовое задание.162
309 users voted. 276 users abstained.