Обновить
1

Пользователь

Отправить сообщение

Ну да. Похоже на дело. НО, лично для меня этот код запутанный и непонятный. Может потому, что все в одном большом методе. Еще есть дублирование кода, например:

$timeParts = explode(':', $parts[0]);
...
$timeParts = explode(':', $parts[1]);
...
$timeParts = explode(':', $parts[2]);

А так, в целом на PHP получается короче, чем на Delphi, признаю.

Знаком с темой, но не считаю себя в ней крутым специалистом. В целом поддерживаю ваши мысли. В данном конкретном случае, в связи с простотой синтаксиса, BNF можно представлять в уме, а шаг 2 (т.е. разбиение на токены) - пропустить и работать прямо с символами.

Ну, давайте прикинем BNF:

<ScheduleString> ::= [<Date> ' '] <Time>
<Date> ::= <Range> '.' <Range> '.' <Range> [' ' <Range>]
<Time> ::= <Range> ':' <Range> ':' <Range> ['.' <Range>]
<Range> ::= <Item> {',' <Item>}
<Item> ::= <Number> | (((<Number> '-' <Number>) | '*') ['/' <Number>])
<Number> ::= <Digit> {<Digit>}
<Digit> ::= {'0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'}

Уговорили, вот вам код с минималистичным формированием результата. С проверкой диапазонов.

Раз уж вы про 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;

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 не стал применять.

Просто пробегаем по всей строке от начала до конца. Ну, пару раз пришлось бы заглянуть вперед. Это и есть нормальная стандартная практика парзинга чего-либо, особенно маленьких строк.

Можно, конечно, разбивать строку на токены, но в данном случае это овекил.

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность