Изящный синтаксический анализ даты в Ruby

#ruby #parsing #datetime

#ruby #синтаксический анализ #дата- время

Вопрос:

У меня есть два параметра даты в действии контроллера, которые я хотел бы вернуть к значению по умолчанию, если они равны нулю или синтаксический анализ завершается неудачей.

К сожалению, кажется, что DateTime.strptime при сбое синтаксического анализа возникает исключение, что вынуждает меня написать это чудовище:

 starting = if params[:starting].present?
  begin
    DateTime.strptime(params[:starting], "%Y-%m-%d")
  rescue
    @meeting_range.first
  end
else
  @meeting_range.first
end
  

Чувствует себя плохим человеком. Есть ли какой-либо способ проанализировать дату с помощью Ruby stdlib, для которого не требуется begin...rescue блок? Хронический кажется излишним в этой ситуации.

Комментарии:

1. Вы можете избавиться от обусловленности, present? если вы сохраняете исключение.

2. Имеет ли отношение к вопросу то, что у вас есть два параметра даты вместо, скажем, одного? Если нет, удаление ненужной информации из вопроса поможет читателю.

3. @sawa цель вопроса — полностью устранить begin...rescue блок. И ваш другой момент — это просто придирки.

4. Идея strptime заключается в том, что вы уже знаете, что синтаксический анализ будет успешным, потому что вы уже определили этот формат даты по мере выполнения кода, прежде чем strptime его увидите. Date#parse , DateTime#parse или Time#parse подходят для определения правильного формата, когда вы не уверены, что получаете, хотя они натыкаются на даты в форматах %m/%d/%Y и %d/%m/%Y .

5. @железный дровосек, чего я хочу, так это функции синтаксического анализа, которая возвращает ноль при сбое вместо выдачи исключения. Date#parse , DateTime#parse и Time#parse все выбрасывают ArgumentError . Я не могу вспомнить ни одной ситуации, когда я хотел бы, чтобы параметр date вел себя таким образом в контроллере.

Ответ №1:

В общем, я не могу согласиться с другим решением, использовать rescue таким образом — плохая практика. Я думаю, это стоит упомянуть на случай, если кто-то другой попытается применить концепцию к другой реализации.

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

Следующее для Date не DateTime , но вы поймете идею:

 Date.parse(home.build_time) # where build_time does not exist or home is nil
Date.parse(calculated_time) # with any exception in calculated_time
  

Столкнувшись с той же проблемой, я закончил тем, что исправил Ruby следующим образом:

 # date.rb
class Date
  def self.safe_parse(value, default = nil)
    Date.parse(value.to_s)
  rescue ArgumentError
    default
  end
end
  

Любое исключение в значении будет увеличено перед входом в метод, и будет перехвачено только ArgumentError (хотя я не знаю ни о каких других возможных).

Единственное правильное использование inline rescue — это нечто подобное этому:

 f(x) rescue handle($!)
  

Обновить

В эти дни я предпочитаю не исправлять Ruby с помощью monkey. Вместо этого я оборачиваю свой Date в Rich модуль, который я вставляю lib/rich , затем вызываю его с помощью:

 Rich::Date.safe_parse(date)
  

Комментарии:

1. Действительно, каждый раз, когда я использовал встроенное спасение, я сожалел об этом. Я уже давно перенес все свои потребности в анализе даты / времени на использование вместо этого своевременности , которая ведет себя так, как я хочу.

Ответ №2:

Почему бы просто не:

 starting = DateTime.strptime(params[:starting], '%Y-%m-%d') rescue @meeting_range.first
  

Комментарии:

1. Это выглядит как приемлемый компромисс, если я должен смириться с использованием rescue . Тем не менее, все еще похоже на антипаттерн.

2. Совершенно очевидно, что сбой при анализе значения вызывает исключение. Аналогично, если вы ожидаете значение, то отсутствие значения также является исключительным состоянием. Ruby этого не делает, но на многих языках даже функция, которая ищет в массиве определенное значение, вызывает исключение, если значения нет в массиве, и обращение к элементу хэша с помощью неправильного ключа также приводит к этому (что в Ruby возвращает nil ). Исключения следует рассматривать не как ошибки, а как неожиданное состояние, отличное от того, что было бы нормальным в данной ситуации. Если вы принимаете это представление, а не антипаттерн.

3. Я считаю антишаблоном несоответствие поведения, а не само исключение. Вы правы в том, что создание исключения в некоторых случаях совершенно разумно. Я просто хотел бы, чтобы здесь была альтернатива, поскольку сбой синтаксического анализа не является неожиданностью в этом контексте.

4. Код значительно очищается с помощью завершающего элемента rescue , но также может открыть новую банку с червями, если что-то помимо синтаксического анализа не удается слева от rescue . Этот случай выглядит довольно безобидным, но с ним следует быть осторожным. Отладка такого рода проблем раздражает / расстраивает / сложна: выберите любые две.

5. @TinMan: Это правда. Вы должны быть уверены, что ваша одна строка вызовет только исключение одного типа (или что вы хотите одинаковую обработку для любого исключения). Но в этом случае единственная неисправная часть — это strptime , и в исходном коде также было неограниченное rescue , поэтому я полагаю, что здесь у нас все должно получиться.

Ответ №3:

В наши дни мой предпочтительный подход заключается в использовании Dry::Types для приведения к типу и Dry::Monads для представления ошибок.

 require "dry/types"
require "dry/monads"
Dry::Types.load_extensions(:monads)
Types = Dry::Types(default: :strict)

Types::Date.try("2021-07-27T12:23:19-05:00")
# => Success(Tue, 27 Jul 2021)

Types::Date.try("foo")
# => Failure(ConstraintError: "foo" violates constraints (type?(Date, "foo"))
  

Ответ №4:

Все существующие ответы где-то есть rescue . Однако мы можем использовать некоторые «уродливые» методы, которые были доступны с версии Ruby 1.9.3 (они были там раньше, но официального описания нет).

Метод уродлив, потому что он начинается с подчеркивания. Тем не менее, это соответствует цели.

С помощью этого можно записать вызов метода в вопросе

 starting = if params[:starting].present?
  parsed = DateTime._strptime(params[:starting], "%Y-%m-%d") || {}
  if parsed.count==3 amp;amp; Date.valid_date?(parsed[:year], parsed[:month], parsed[:mday])
    @meeting_range.first
  end
else
    @meeting_range.first
end

  
  • Если строка даты соответствует входному формату, _strptime вернет хэш со всеми 3 частями даты. это parsed.count==3 означает, что все 3 части существуют.
  • Однако дальнейшая проверка того, что три части образуют действительную дату в календаре, по-прежнему необходима, поскольку _strptime вам не скажут, что они недействительны.

Ответ №5:

Когда вы хотите получить дату как объект, проанализированный из строковой переменной, иногда передаваемое строковое значение может быть нулевым, или пустым, или недопустимой строкой даты. Я бы хотел для краткости написать безопасные методы:

 def safe_date(string_date)
  ::Date.parse(string_date)
rescue TypeError, ::Date::Error
  ::Date.today
end
  

Например — проверка в консоли irb:

 3.0.2 :001 > safe_date
 => #<Date: 2022-08-29 ((2459821j,0s,0n), 0s,2299161j)>
3.0.2 :001 > safe_date('')
 => #<Date: 2022-08-29 ((2459821j,0s,0n), 0s,2299161j)> 
3.0.2 :002 > safe_date('29.12.2022')
 => #<Date: 2022-12-29 ((2459943j,0s,0n), 0s,2299161j)>
3.0.2 :003 > safe_date('29.13.2022')
 => #<Date: 2022-08-29 ((2459821j,0s,0n), 0s,2299161j)>
  

Комментарии:

1. Молчаливое игнорирование подобных сбоев неизбежно вызовет проблемы. Я рекомендую использовать типы результатов, подобные моему принятому ответу.