Есть две части, чтобы заставить это работать, и третья часть, чтобы заставить все это работать хорошо. Если вы просто хотите скопировать вставку, продолжайте и прокрутите до конца.
Часть 1: Получение Date
для форматирования по желанию
В ActiveSupport имеется базовых расширений для Date на , но ни одно из них не создает новый класс дат. Если вы работаете с Date
, то есть то, что Rails возвращает, когда в вашей базе данных есть столбец date
, метод преобразования этой даты в строку - Date#to_formatted_s
. Расширения ActiveSupport
добавляют этот метод, и они псевдоним метода to_s
, поэтому, когда вы вызываете Date#to_s
(вероятно, неявно), вы получаете Date.to_formatted_s
.
Существует два способа изменить формат, используемый to_formatted_s
:
- Передайте имя формата, который вы хотели бы использовать (этот аргумент будет использоваться для поиска формата в
Date::DATE_FORMATS
).
Изменить формат, на который отображается :default
. Поскольку to_formatted_s
устанавливает значение по умолчанию :default
, при вызове Date#to_s
без передачи аргумента вы получаете формат, заданный Date::DATE_FORMATS[:default]
. Вы можете переопределить настройки по умолчанию для всего приложения следующим образом:
Date::DATE_FORMATS[:default] = "%m/%d/%Y"
У меня есть этот набор в инициализаторе, как config/initializers/date_formats.rb
. Он грубый и не поддерживает изменения с вашими локализациями, но заставляет Date#to_s
возвращать желаемый формат без каких-либо махинационных исправлений или явного преобразования ваших дат в строки и передачи аргумента.
Часть 2. Получение введенных пользователем дат, преобразованных в объекты Date
По умолчанию ваш экземпляр ActiveRecord
будет столбцы даты приведения , используя добрый старый ISO 8601, который выглядит следующим образом: yyyy-mm-dd
.
Если он не точно соответствует этому формату (например, он разделен косой чертой), он прибегает к использованию Date._parse в Ruby, который немного мягче, но все еще ожидает увидеть дни, месяцы, затем годы: dd/mm/yyyy
.
Чтобы Rails анализировал даты в формате, который вы собираете, вам просто нужно заменить метод cast_value
на ваш собственный. Вы можете monkeypatch ActiveRecord::Types::Date
, но не намного сложнее бросить свой собственный тип, унаследованный от него.
Мне нравится использовать Chronic для разбора строк дат из пользовательского ввода, потому что он терпит больше дерьма, например, "Tomorrow" или "Jan 1 99", или даже вводные помехи, такие как "5 / 6- 99 "(6 мая 1999 г.). Поскольку Chronic возвращает экземпляр Time
и мы переопределяем предоставленный метод cast_value
, нам нужно вызвать to_date
для него.
class EasyDate < ActiveRecord::Type::Date
def cast_value(value)
default = super
parsed = Chronic.parse(value)&.to_date if value.is_a?(String)
parsed || default
end
end
Теперь нам просто нужно указать ActiveRecord использовать EasyDate
вместо ActiveRecord::Type::Date
:
class Pony < ApplicationRecord
attribute :birth_date, EasyDate.new
end
Это работает, но если вы похожи на меня, вы хотите взять на себя больше работы прямо сейчас, чтобы вы могли сэкономить 3 секунды в будущем. Что если бы мы могли сказать ActiveRecord всегда использовать EasyDate для столбцов, которые являются просто датами? Мы можем.
Часть 3: Сделайте так, чтобы Rails обрабатывал это автоматически
ActiveRecord предоставляет вам всевозможную информацию о вашей модели, которую он использует для обработки всей «магии» за кулисами. Один из методов, который он нам дает attribute_types
Если вы запускаете это в своей консоли, вы получаете рвоту - это отчасти страшно, и вы просто говорите «о, неважно». Однако если вы покопаетесь в этой рвоте как какой-то чудак, то это просто хеш, который выглядит так:
{ column_name: instance_of_a_type_object, ... }
Не страшно, но, поскольку мы начинаем тыкать во внутренние органы, проверенный вывод этих instance_of_a_type_object
может в конечном итоге выглядеть тупым и «закрытым». Как это:
#<ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Money:0x007ff974d62688
К счастью, мы действительно заботимся только об одном: ActiveRecord::Types::Date
, поэтому мы можем сузить его:
date_attributes = attribute_types.select { |_, type| ActiveRecord::Type::Date === type }
Это дает нам список атрибутов, которые мы хотим использовать для использования EasyDate
. Теперь нам нужно просто указать ActiveRecord использовать EasyDate для этих атрибутов, как мы делали выше:
date_attributes.each do |attr_name, _type|
attribute attr_name, EasyDate.new
end
И это в основном делает это, но нам нужно склеить все это вместе. Если вы используете ApplicationRecord
, вы можете бросить это прямо в него и отправиться в гонки:
def inherited(klass)
super
klass.attribute_types.select { |_, type| ActiveRecord::Type::Date === type }.each do |name, _type|
klass.attribute name, EasyDate.new
end
end
Если вы делаетеЯ не хочу «загрязнять» ApplicationRecord
, вы можете сделать, как я, создать модуль и расширить с ним ApplicationRecord
. Если вы не используете ApplicationRecord
, вам нужно создать модуль и расширить ActiveRecord::Base
.
module FixDateFormatting
# the above `inherited` method here
end
# Either this...
class ApplicationRecord < ActiveRecord::Base
extend FixDateFormatting
end
# ...or this:
ActiveRecord::Base.extend(FixDateFormatting)
TL; DR - Что копировать вставить
Если вас не интересует как и вы просто хотите, чтобы это работало, вы можете:
- Добавьте
gem "chronic"
к вашему Gemfile и bundle install
- Убедитесь, что ваши модели наследуются от
ApplicationRecord
- Скопируйте код ниже файла в
config/initializers/date_formatting.rb
- Перезапуск
- Все должно теперь просто работать ™
Вот код для копирования:
Date::DATE_FORMATS[:default] = "%m/%d/%Y"
module FixDateFormatting
class EasyDate < ActiveRecord::Type::Date
def cast_value(value)
default = super
parsed = Chronic.parse(value)&.to_date if value.is_a?(String)
parsed || default
end
end
def inherited(subclass)
super
date_attributes = subclass.attribute_types.select { |_, type| ActiveRecord::Type::Date === type }
date_attributes.each do |name, _type|
subclass.attribute name, EasyDate.new
end
end
end
Rails.application.config.to_prepare do
ApplicationRecord.extend(FixDateFormatting)
end