Ну да. Похоже на дело. НО, лично для меня этот код запутанный и непонятный. Может потому, что все в одном большом методе. Еще есть дублирование кода, например:
Знаком с темой, но не считаю себя в ней крутым специалистом. В целом поддерживаю ваши мысли. В данном конкретном случае, в связи с простотой синтаксиса, BNF можно представлять в уме, а шаг 2 (т.е. разбиение на токены) - пропустить и работать прямо с символами.
Уговорили, вот вам код с минималистичным формированием результата. С проверкой диапазонов.
Раз уж вы про CQS упоминаете, то позволю себе просто возвращать диапазоны колбэком. Так как не парзера это дело, всякие битовые массивы для поиска заполнять.
TScheduleParser (Delphi)
type
TPart = (pYear, pMonth, pDay, pWeek, pHour, pMinute, pSecond,
pMillisec);
TAddItem = procedure(APart: TPart; AMin, AMax, AStep: Integer) of object;
TScheduleParser = class
private
FCurrent: PChar;
FResult: TAddItem;
function LookAheadOfList: Char;
procedure ParseList(APart: TPart);
procedure ParseListItem(APart: TPart);
procedure ParseNumber(var N: Integer);
procedure Skip; overload;
procedure Skip(ARequired: Char); overload;
procedure SyntaxError;
procedure AddItem(APart: TPart; AMin, AMax, AStep: Integer);
public
procedure Parse(const S: string; const AResult: TAddItem);
end;
procedure TScheduleParser.AddItem(APart: TPart; AMin, AMax, AStep: Integer);
type
TPartBounds = record
Min, Max, DefMax: Integer;
end;
const
PART_BOUNDS: array[TPart] of TPartBounds = (
(Min: 2000; Max: 2100; DefMax: 2100), // pYear
(Min: 1; Max: 12; DefMax: 12), // pMonth
(Min: 1; Max: 32; DefMax: 31), // pDay
(Min: 0; Max: 6; DefMax: 6), // pWeek
(Min: 0; Max: 23; DefMax: 23), // pHour
(Min: 0; Max: 59; DefMax: 59), // pMinute
(Min: 0; Max: 59; DefMax: 59), // pSecond
(Min: 0; Max: 999; DefMax: 999) // pMillisec
);
begin
if AMin = -1 then
begin
AMin := PART_BOUNDS[APart].Min; // '*'
AMax := PART_BOUNDS[APart].DefMax; //
end
else
begin
if AMin < PART_BOUNDS[APart].Min then
SyntaxError;
if AMax = -1 then
AMax := AMin
else if AMax > PART_BOUNDS[APart].Max then
SyntaxError;
end;
if AStep <= 0 then
SyntaxError;
if Assigned(FResult) then
FResult(APart, AMin, AMax, AStep);
end;
function TScheduleParser.LookAheadOfList: Char;
var
c: PChar;
begin
c := Self.FCurrent;
while c^ in ['*', ',', '-', '/', '0'..'9'] do
Inc(c);
Result := c^;
end;
procedure TScheduleParser.Parse(const S: string; const AResult: TAddItem);
begin
if S = '' then
SyntaxError;
FCurrent := Pointer(S);
FResult := AResult;
if LookAheadOfList = '.' then // Date.
begin
ParseList(pYear);
Skip('.');
ParseList(pMonth);
Skip('.');
ParseList(pDay);
Skip(' ');
if LookAheadOfList = ' ' then // Week day.
begin
ParseList(pWeek);
Skip(' ');
end;
end;
ParseList(pHour); // Time.
Skip(':'); //
ParseList(pMinute); //
Skip(':'); //
ParseList(pSecond); //
if FCurrent^ = '.' then // Milliseconds.
begin
Skip;
ParseList(pMillisec);
end;
Skip(#0); // Check the end of the string.
end;
procedure TScheduleParser.ParseNumber(var N: Integer);
var
dgt: Boolean;
begin
dgt := False;
N := 0;
while FCurrent^ in ['0'..'9'] do
begin
N := N * 10 + Ord(FCurrent^) - Ord('0');
dgt := True;
Skip;
end;
if not dgt then
SyntaxError;
end;
procedure TScheduleParser.ParseList(APart: TPart);
begin
ParseListItem(APart);
while FCurrent^ = ',' do
begin
Skip;
ParseListItem(APart);
end;
end;
procedure TScheduleParser.ParseListItem(APart: TPart);
var
rng: Boolean;
min, max, stp: Integer;
begin
min := -1; // Full range '*'
max := -1; //
stp := 1; //
if FCurrent^ = '*' then // Range.
begin
Skip;
rng := True;
end
else
begin
ParseNumber(min);
if FCurrent^ = '-' then // Range.
begin
Skip;
ParseNumber(max);
rng := True;
end
else
rng := False;
end;
if rng and (FCurrent^ = '/') then // Range step.
begin
Skip;
ParseNumber(stp);
end;
AddItem(APart, min, max, stp);
end;
procedure TScheduleParser.Skip(ARequired: Char);
begin
if FCurrent^ <> ARequired then
SyntaxError;
Inc(FCurrent);
end;
procedure TScheduleParser.Skip;
begin
Inc(FCurrent);
end;
procedure TScheduleParser.SyntaxError;
begin
raise Exception.Create('Syntax error in schedule string');
end;
Этот результат можно использовать в дальнейшей программной обработке
С таким подходом можно вообще входную строку вернуть как есть, ничего не делая, и заявить, что это результат и его можно дальше программно обрабатывать.
Можно даже строковые ключи сделать для наглядности.
Нельзя, так как вы даже не знаете из скольких частей у вас состоит дата, а также время.
Даже со всеми нужными проверками у меня получилось 60 строк кода.
Для начала покажите код "со всеми нужными проверками".
Давайте так, у меня нет проверки диапазона допустимых значений просто потому, что это традиционно считается не частью синтаксиса, а частью семантики. Во всех остальных случаях мой код весь синтаксис проверяет, и если строка будет неправильной, он ругнется. Проверку допустимых значений могу добавить, если очень хочется.
Ваш же код не проверяет огромное количество случаев. Я уже привел примеры. Вот Вы пишете про то, что вместо числа может быть звездочка. Ну и что? Согласно Вашему коду там вообще все что угодно можно написать вместо числа, например буквы и ничего не случится.
Время можно написать, например так: 12:AB:CD:XY:SD и ошибки Ваш код не выдаст.
А можно написать диапазон вот так: *-10/3, что неправильно, т.к. после звездочки не может быть тире.
А можно даже так: 3-10/3/4/5/6/7 и Ваш код не ругнется.
Честно признаться, там и половины нет тех проверок, которые должны быть.
В начале Вы проверили, что частей должно быть от одной до трех. А дальше? Там еще много explode() и ни одной проверки к ним.
Вы не парзите числа. То есть, там, между разделителями, можно написать все что угодно.
Звездочку вместо числа тоже не проверяете. А между тем, звездочка сама по себе означает диапазон, и после нее второго числа через тире уже быть не может.
Может еще чего не заметил. Но, в том то и фишка - в методе на сплитах нужно задолбаться с проверками после каждого шага и нигде их не забыть. А у меня само собой все работает, ну почти :).
Возвращать результат нужно в каком-то определенном виде. Поскольку этот вид зависит от дальнейшего использования (от алгоритма поиска), а мы вроде как говорим про парзинг, я решил формирование результата опустить.
Валидацию ренджей легко добавить, это почти не увеличит размер кода. Хотя строчки я ну так, примерно, посчитал. Просто чтобы показать что такой низкоуровневый код не будет очень уж сильно больше. Т.е. не будет, например, 1000 строчек.
Что за "и т.д., и т.п." - мне непонятно. Код корректный.
Ладно, ладно, количество строчек сравнивать некорректно. Ваш код еще посмотрю, хотя я PHP не знаю. Но, я же в целом отвечал тем, кто предлагает String.Split() использовать и регэкспы.
Теперь давайте представим, как бы это выглядело на String.Split() и Int.Parse(). Следите за руками:
1) Сплиттим по пробелу - получаем максимум три части.
2) Сплиттим дату по точке.
3) Сплиттим время по двоеточию.
4) Сплиттим секунды+миллисекунды по точке.
5) Получили восемь списков. Каждый из них сплиттим по запятой. Получаем диапазоны.
6) Диапазоны сплиттим по символу '/', а потом еще раз по символу '-'.
7) Ну и наконец используем Int.Parse для всех чисел.
А теперь возьмем, например, строку: "2021.07.07 06 12:01.01.333".
Давайте посчитаем для нее количество операций. Получится, если я не ошибаюсь, двадцать восемь сплиттов, которые создают шестьдесят два объекта (строки и массивы) в куче.
Это ли не шизофрения!
И я более чем уверен, что если вы все это напишете на C#, получится громоздкий и не читаемых код, по сравнению с моим, который к тому же еще и более гибкий (на случай изменения формата в будущем), не говоря уже про то, что несравнимо более производительный.
type
TScheduleParser = class
private
function LookAheadOfList(C: PChar): Char;
procedure ParseList(var C: PChar);
procedure ParseListItem(var C: PChar);
procedure ParseNumber(var C: PChar; var N: Integer);
procedure Skip(var C: PChar); overload;
procedure Skip(var C: PChar; ARequired: Char); overload;
procedure SyntaxError;
public
procedure Parse(const S: string);
end;
function TScheduleParser.LookAheadOfList(C: PChar): Char;
begin
while C^ in ['*', ',', '-', '/', '0'..'9'] do
Inc(C^);
Result := C^;
end;
procedure TScheduleParser.Parse(const S: string);
var
c: PChar;
begin
if S = '' then
SyntaxError;
c := Pointer(S);
if LookAheadOfList(c) = '.' then // Date.
begin
ParseList(c);
Skip(c, '.');
ParseList(c);
Skip(c, '.');
ParseList(c);
Skip(c, ' ');
if LookAheadOfList(c) = ' ' then // Week day.
begin
ParseList(c);
Skip(c, ' ');
end;
end;
ParseList(c); // Time.
Skip(c, ':'); //
ParseList(c); //
Skip(c, ':'); //
ParseList(c); //
if c^ = '.' then // Milliseconds.
begin
Skip(c);
ParseList(c);
end;
Skip(c, #0); // Check the end of the string.
end;
procedure TScheduleParser.ParseNumber(var C: PChar; var N: Integer);
var
dgt: Boolean;
begin
dgt := False;
N := 0;
while C^ in ['0'..'9'] do
begin
N := N * 10 + Ord(C^) - Ord('0');
dgt := True;
Inc(C);
end;
if not dgt then
SyntaxError;
end;
procedure TScheduleParser.ParseList(var C: PChar);
begin
ParseListItem(C);
while C^ = ',' do
begin
Skip(C);
ParseListItem(C);
end;
end;
procedure TScheduleParser.ParseListItem(var C: PChar);
var
rng: Boolean;
dummy: Integer;
begin
if C^ = '*' then // Range.
begin
Skip(C);
rng := True;
end
else
begin
ParseNumber(c, dummy);
if C^ = '-' then // Range.
begin
Skip(C);
ParseNumber(c, dummy);
rng := True;
end
else
rng := False;
end;
if rng and (C^ = '/') then // Range step.
begin
Skip(C);
ParseNumber(C, dummy);
end;
end;
procedure TScheduleParser.Skip(var C: PChar; ARequired: Char);
begin
if C^ <> ARequired then
SyntaxError;
Inc(C);
end;
procedure TScheduleParser.Skip(var C: PChar);
begin
Inc(C);
end;
procedure TScheduleParser.SyntaxError;
begin
raise Exception.Create('Syntax error in schedule string');
end;
Хотите я вам парзинг того, что нужно в этой статье за час напишу? Только на Delphi (не люблю C#).
Не используя утилит из стандартной библиотеки языка вообще. И не создавая дополнительных объектов в куче (в отличие от регэксп и всяких там String.Split).
Согласен в Вами. Народ, какие регэкспы, вы чо с ума посходили? Я думаю, что внутри DateTime.Parse даже Int32.Parse не используется. В данном случае я бы и String.IndexOf не стал применять.
Просто пробегаем по всей строке от начала до конца. Ну, пару раз пришлось бы заглянуть вперед. Это и есть нормальная стандартная практика парзинга чего-либо, особенно маленьких строк.
Можно, конечно, разбивать строку на токены, но в данном случае это овекил.
Ну да. Похоже на дело. НО, лично для меня этот код запутанный и непонятный. Может потому, что все в одном большом методе. Еще есть дублирование кода, например:
А так, в целом на PHP получается короче, чем на Delphi, признаю.
Знаком с темой, но не считаю себя в ней крутым специалистом. В целом поддерживаю ваши мысли. В данном конкретном случае, в связи с простотой синтаксиса, BNF можно представлять в уме, а шаг 2 (т.е. разбиение на токены) - пропустить и работать прямо с символами.
Ну, давайте прикинем BNF:
Уговорили, вот вам код с минималистичным формированием результата. С проверкой диапазонов.
Раз уж вы про CQS упоминаете, то позволю себе просто возвращать диапазоны колбэком. Так как не парзера это дело, всякие битовые массивы для поиска заполнять.
TScheduleParser (Delphi)
С таким подходом можно вообще входную строку вернуть как есть, ничего не делая, и заявить, что это результат и его можно дальше программно обрабатывать.
Нельзя, так как вы даже не знаете из скольких частей у вас состоит дата, а также время.
Для начала покажите код "со всеми нужными проверками".
Ну хорошо, пусть Ваш код делает то, что делает. Я бы ЭТО вообще никогда не назвал парзером, так как он тупо не проверят синтаксис.
У Вас тоже нет результата. Т.к. то что ваш код формирует, это жесть, а не результат.
Давайте так, у меня нет проверки диапазона допустимых значений просто потому, что это традиционно считается не частью синтаксиса, а частью семантики. Во всех остальных случаях мой код весь синтаксис проверяет, и если строка будет неправильной, он ругнется. Проверку допустимых значений могу добавить, если очень хочется.
Ваш же код не проверяет огромное количество случаев. Я уже привел примеры. Вот Вы пишете про то, что вместо числа может быть звездочка. Ну и что? Согласно Вашему коду там вообще все что угодно можно написать вместо числа, например буквы и ничего не случится.
Время можно написать, например так: 12:AB:CD:XY:SD и ошибки Ваш код не выдаст.
А можно написать диапазон вот так: *-10/3, что неправильно, т.к. после звездочки не может быть тире.
А можно даже так: 3-10/3/4/5/6/7 и Ваш код не ругнется.
Честно признаться, там и половины нет тех проверок, которые должны быть.
Вы не полностью проверяете синтаксис:
В начале Вы проверили, что частей должно быть от одной до трех. А дальше? Там еще много explode() и ни одной проверки к ним.
Вы не парзите числа. То есть, там, между разделителями, можно написать все что угодно.
Звездочку вместо числа тоже не проверяете. А между тем, звездочка сама по себе означает диапазон, и после нее второго числа через тире уже быть не может.
Может еще чего не заметил. Но, в том то и фишка - в методе на сплитах нужно задолбаться с проверками после каждого шага и нигде их не забыть. А у меня само собой все работает, ну почти :).
Возвращать результат нужно в каком-то определенном виде. Поскольку этот вид зависит от дальнейшего использования (от алгоритма поиска), а мы вроде как говорим про парзинг, я решил формирование результата опустить.
Валидацию ренджей легко добавить, это почти не увеличит размер кода. Хотя строчки я ну так, примерно, посчитал. Просто чтобы показать что такой низкоуровневый код не будет очень уж сильно больше. Т.е. не будет, например, 1000 строчек.
Что за "и т.д., и т.п." - мне непонятно. Код корректный.
Ладно, ладно, количество строчек сравнивать некорректно. Ваш код еще посмотрю, хотя я PHP не знаю. Но, я же в целом отвечал тем, кто предлагает String.Split() использовать и регэкспы.
Да, Вы правы.
Теперь давайте представим, как бы это выглядело на String.Split() и Int.Parse(). Следите за руками:
1) Сплиттим по пробелу - получаем максимум три части.
2) Сплиттим дату по точке.
3) Сплиттим время по двоеточию.
4) Сплиттим секунды+миллисекунды по точке.
5) Получили восемь списков. Каждый из них сплиттим по запятой. Получаем диапазоны.
6) Диапазоны сплиттим по символу '/', а потом еще раз по символу '-'.
7) Ну и наконец используем Int.Parse для всех чисел.
А теперь возьмем, например, строку: "2021.07.07 06 12:01.01.333".
Давайте посчитаем для нее количество операций. Получится, если я не ошибаюсь, двадцать восемь сплиттов, которые создают шестьдесят два объекта (строки и массивы) в куче.
Это ли не шизофрения!
И я более чем уверен, что если вы все это напишете на C#, получится громоздкий и не читаемых код, по сравнению с моим, который к тому же еще и более гибкий (на случай изменения формата в будущем), не говоря уже про то, что несравнимо более производительный.
удалено.
Выложу прямо тут
2020.07.12 1.000:00:00
Чисто парзинг, без всего остального:
https://www.file.io/download/4hZfwBRjjCRn
150 строчек вместе с объявлением класса. Даже короче чем у автора статьи. А на C# еще короче получится.
Эти разделители, к слову, конфликтующие с синтаксисом в задании, еще одна причина не использовать Int32.Parse().
Хотите я вам парзинг того, что нужно в этой статье за час напишу? Только на Delphi (не люблю C#).
Не используя утилит из стандартной библиотеки языка вообще. И не создавая дополнительных объектов в куче (в отличие от регэксп и всяких там String.Split).
Согласен в Вами. Народ, какие регэкспы, вы чо с ума посходили? Я думаю, что внутри DateTime.Parse даже Int32.Parse не используется. В данном случае я бы и String.IndexOf не стал применять.
Просто пробегаем по всей строке от начала до конца. Ну, пару раз пришлось бы заглянуть вперед. Это и есть нормальная стандартная практика парзинга чего-либо, особенно маленьких строк.
Можно, конечно, разбивать строку на токены, но в данном случае это овекил.