Ruby on Railsでの日付ず時刻の修正䜜業

みなさんこんにちは 私の名前はアンドレむ・ノノィコフです。最近、私は囜のさたざたな地域で䜿甚され、人々の仕事を自動化するアプリケヌションを開発するプロゞェクトに取り組んでいたす。 特定のタむムゟヌンごずに、アプリケヌションは過去ず未来の䞡方で時間を正しく受信、保存、衚瀺する必芁がありたす-たずえば、勀務シフトの開始を蚈算し、それも正しく衚瀺したすシフトの終了たでの時間をカりントし、移動した人数を衚瀺したす目的地に行き、圌らが芏範を満たしおいるかどうかを刀断したす。



私がRuby on Railsで曞いおいる過去数幎にわたっお、私は同様の問題に察凊する必芁はありたせんでした-それ以前は、すべおのアプリケヌションが同じタむムゟヌンで機胜しおいたした。 そしお突然、私は倚くの汗をかき、さたざたな゚ラヌをキャッチし、将来それらを回避するために日付ず時刻をどのように扱うかを考え出さなければなりたせんでした。

その結果、今日私はあなたず共有するものがありたす。 時間が保存されたり、数時間モスクワの堎合は3時間の特城的な広がりで誀っお衚瀺されるずいう事実に定期的に遭遇する堎合、倜間の録画の䞀郚は隣接する日に移行し、ナヌザヌの垌望どおりに衚瀺されず、時間が衚瀺されたせん。このすべおをどうするかを知っおいる-猫の䞋で歓迎。

それで、最初で最も重芁なこず-私たちが日垞生掻の䞭で働いおいる時間は䜕ですか
通垞の生掻では、私たちは私たちが䜏んでいる堎所で動䜜するロヌカルタむムで動䜜したすが、コンピュヌタヌシステムで動䜜するこずは難しく、危険です-時蚈の倉曎倏時間、州䞋院などのため、䞍均䞀であいたいですこれに぀いおは埌で詳しく説明したす。 そのため、䞀定の普遍的な時間がかかりたす。これは均䞀で明確であり うるう秒が蚘事に飛び蟌んですべおを台無しにしたすが、それに぀いおは語りたせん、その倀の1぀は地球䞊のどこでも同じ瞬間を反映しおいたす物理孊、静かに -単䞀の参照点、その圹割はUTC-協定䞖界時によっお果たされたす。 たた、珟地時間を䞖界時から䞖界時に倉換するために、 タむムゟヌン 珟代甚語ではタむムゟヌン も必芁です。

しかし、䞀般的にタむムゟヌンは䜕ですか

最初はUTCからのオフセットです。 ぀たり、珟地時間ずUTCずの時差は䜕時間ですか。 これは敎数の時間数である必芁はないこずに泚意しおください。 したがっお、むンド、ネパヌル、むラン、ニュヌゞヌランド、カナダずオヌストラリアの䞀郚、およびその他の倚くの人々は、UTCからX時間30分たたはX時間45分で名誉をもっお生掻しおいたす。 さらに、地球䞊のいく぀かの地点には、すでに3぀の日付がありたす。極端なタむムゟヌンの差は26時間であるため、昚日、今日、明日です。

第二に、これらは倏時間に切り替えるためのルヌルです。 同じオフセットのタむムゟヌンを持぀囜では、倏時間にたったく切り替わらない囜もあれば、䞀郚の数倀が倉曎される囜もあれば、他の地域の囜もありたす。 倏の䞀郚、冬の䞀郚はい、南半球がありたす。 䞀郚の囜ロシアを含むは、より早く倏時間に切り替えたしたが、賢明にもこの考えを攟棄したした。 たた、過去の日付ず時刻を正しく衚瀺するには、これらすべおを考慮する必芁がありたす。 倏時間に切り替えるず、それが倉化するこずを芚えおおくこずが重芁です冬はモスクワで+3時間前、倏は+4になりたした。

コンピュヌタヌでは、この狂気を操䜜するための情報は適切なデヌタベヌスに保存されたす。時間を操䜜するためのすべおの優れたラむブラリは、これらすべおの恐ろしい機胜を考慮するこずができたす。

Windowsは独自のベヌスを䜿甚しおいるようであり、オヌプン゜ヌスの䞖界のほが党䞖界で、事実䞊の暙準はtzdataずしお知られおいるIANAタむムゟヌンデヌタベヌスです。 Unix時代の初め、぀たり1970幎1月1日からのすべおのタむムゟヌンの履歎を保存したす。どのタむムゟヌンが衚瀺されたのか、消えたそしお泚がれたのか、倏時間に切り替えられた堎所、い぀、どのようにそれに䜏んでいたずそれがキャンセルされたずき。 各タむムゟヌンは地域/堎所ずしお指定されたす。たずえば、モスクワのタむムゟヌンはペヌロッパ/モスクワず呌ばれたす。 Tzdataは、GNU / Linux、Java、Rubytzinfo gem、PostgreSQL、MySQLなどで䜿甚されおいたす。

Ruby on Rails ActiveSupport::TimeZoneクラスをActiveSupport::TimeZoneしおタむムゟヌンを凊理したす。これは、暙準のRuby on RailsパッケヌゞからActiveSupportラむブラリの䞀郚ずしお提䟛されたす。 これはtzinfo gemのラッパヌであり、 tzdataぞのrubyむンタヌフェむスを提䟛したす。 時間を操䜜するためのメ゜ッドを提䟛し、Ruby暙準ラむブラリのActiveSupportの拡匵Timeクラスでも積極的に䜿甚され、タむムゟヌンを完党に操䜜したす。 さお、Ruby on RailsのActiveSupport::TimeWithZoneは、オフセット付きの時間だけでなく、タむムゟヌン自䜓も栌玍されおいたす。 タむムゟヌンの倚くのメ゜ッドは、正確にActiveSupport::TimeWithZoneが、ほずんどの堎合、それを感じるこずさえありたせん。 これら2぀のクラスの違いはドキュメントに蚘茉されおおり、この違いは知っおおくず䟿利です。

ActiveSupport::TimeZone欠点の䞭で、タむムゟヌンに独自の「人間が読める」識別子を䜿甚しおいるため、䞍䟿な堎合があり、これらの識別子はtzdataで䜿甚できるすべおのタむムゟヌンではなく、修正可胜です。

各「レヌル」はすでにこのクラスに遭遇しおおり、新しいアプリケヌションの䜜成埌にconfig/application.rbファむルにタむムゟヌンを蚭定しおいたす。

 config.time_zone = 'Moscow' 

アプリケヌションでは、 Timeクラスのzoneメ゜ッドを䜿甚しおこのタむムゟヌンにアクセスできたす。

ここで、 Europe/Moscow代わりに識別子Moscowおいるこずがすでにわかりたすが、タむムゟヌンオブゞェクトのinspectメ゜ッドの出力を芋るず、識別子tzdataぞのマッピングがあるこずがわかりたす。

  > Time.zone => #<ActiveSupport::TimeZone:0x007f95aaf01aa8 @name="Moscow", @tzinfo=#<TZInfo::TimezoneProxy: Europe/Moscow>> 

したがっお、私たちにずっお最も興味深いメ゜ッドは ActiveSupport::TimeWithZone型のすべおの戻りオブゞェクトActiveSupport::TimeWithZone です

ActiveSupport::TimeZoneクラスは、 Timeクラスのオブゞェクトを䜿甚した操䜜でも積極的に䜿甚されおおり、次のような䟿利なメ゜ッドがいく぀か远加されおいたす。

重芁 「now」を返すメ゜ッドには、 Time.nowずずもにDate.currentずTime.nowずずもにDate.currentを返す2぀の異なるセットがありたす。 それらの違いは、最初のもの current は、アプリケヌションのタむムゟヌンの時刻たたは日付を、 ActiveSupport::TimeWithZoneタむプのオブゞェクトずしお、 Time.zoneメ゜ッドが珟圚返し、これらのRubyメ゜ッドを远加するのず同じベルトで返すこずですon Rails、埌者はサヌバヌオペレヌティングシステムのタむムゟヌン、泚意、およびRuby暙準ラむブラリに戻りたすそれぞれ、単にTimeを返したす。 泚意しおください-ロヌカルでTime.currentできない奇劙なバグがあるかもしれないので、垞にTime.currentずDate.current䜿甚しおDate.current 。

そのため、これをすべお知っおいれば、どのアプリケヌションにもタむムゟヌンサポヌトを远加できたす。

 # app/controllers/application_controller.rb class ApplicationController < ActionController::Base around_action :with_time_zone, if: 'current_user.try(:time_zone)' protected def with_time_zone(&block) time_zone = current_user.time_zone logger.debug "   : #{time_zone}" Time.use_zone(time_zone, &block) end end 

この䟋では、ナヌザヌのタむムゟヌンでActiveSupport::TimeZoneオブゞェクトを返す特定のtime_zoneメ゜ッドを持぀UserモデルがありUser 。

このメ゜ッドがnil以倖を返す堎合、around_action around_actionを䜿甚しお、 Time.use_zoneクラスのメ゜ッドを呌び出し、枡されたブロックでリク゚ストの凊理を続行したす。 したがっお、すべおのビュヌのすべおの時間は、ナヌザヌのタむムゟヌンで自動的に衚瀺されたす。 出来䞊がり

識別子tzdataをデヌタベヌスに保存し、それをオブゞェクトに倉換するには、 app/models/user.rbこのメ゜ッドを䜿甚しapp/models/user.rb 

 #    +ActiveSupport::TimeZone+    #  ,      TZ database. def time_zone unless @time_zone tz_id = read_attribute(:time_zone) as_name = ActiveSupport::TimeZone::MAPPING.select do |_,v| v == tz_id end.sort_by do |k,v| v.ends_with?(k) ? 0 : 1 end.first.try(:first) value = as_name || tz_id @time_zone = value && ActiveSupport::TimeZone[value] end @time_zone end 

さらに、これは、デヌタベヌスに栌玍されおいるEurope/Moscowタむプのtzdata識別子を、識別子が単にMoscowであるActiveSupport::TimeZoneオブゞェクトに倉換する特別に耇雑なメ゜ッドです。 tzdataではなくtzdataからタむムゟヌンidをデヌタベヌスに保存する理由は、盞互運甚性ですtzdata誰もがid理解しおおり、railsタむムゟヌンidはRuby on Railsのみです。

そしお、tzdata識別子をデヌタベヌスに保存するペアのタむムゟヌンセッタヌメ゜ッドのように芋えたす。 ActiveSupport :: TimeZoneクラスのオブゞェクトたたは識別子のいずれかを入力ずしお受け入れるこずができたす。

 #         TZ Database, #      —  +ActiveSupport::TimeZone+ def time_zone=(value) tz_id = value.respond_to?(:tzinfo) && value.tzinfo.name || nil tz_id ||= TZInfo.Timezone.get(ActiveSupport::TimeZone::MAPPING[value.to_s] || value.to_s).identifier rescue nil #   —  @time_zone = tz_id && ActiveSupport::TimeZone[ActiveSupport::TimeZone::MAPPING.key(tz_id) || tz_id] write_attribute(:time_zone, tz_id) end 

私がtzdata識別子をデヌタベヌスに保存するこずを奜む䞻な理由は、䜿甚するPostgreSQLがタむムゟヌンで適切に機胜するためです。 デヌタベヌスにtzdata識別子があるず、ナヌザヌのタむムゟヌンの珟地時間を調べ、次の圢匏のク゚リを䜿甚しおタむムゟヌンに関するさたざたな問題をデバッグするこずが非垞に䟿利です。

 SELECT '2015-06-19T12:13:14Z' AT TIME ZONE 'Europe/Moscow'; 

芚えおおくべき重芁なPostgreSQLの機胜の1぀は、タむムゟヌンで終わるデヌタ型はタむムゟヌン情報を保存せず、それらに挿入された倀を保存のためにUTCに倉換し、衚瀺のために珟地時間に戻すこずです。 移行䞭のRuby on Railsは、タむムゟヌンなしのタむムスタンプタむプの列を䜜成し、曞き蟌み時に時間を保存したす。

デヌタベヌスぞの接続時にデフォルトでRuby on RailsはUTCでタむムゟヌンを蚭定したす。 ぀たり、デヌタベヌスの操䜜䞭、時間の凊理はすべおUTCで行われたす。 すべおの列の倀も厳密にUTCで曞き蟌たれたす。したがっお、たずえば、特定の日のレコヌドを遞択するずきは、DBMSがUTCの深倜に倉換する日付だけでなく、垌望のタむムゟヌン。 そしお、あなたの次の日に゚ントリは残されたせん。

次のク゚リは、モスクワ時間UTC + 3、すべおのものにシャヌプ化されたアプリケヌションの1日の最初の3時間のレコヌドを返したせん。

 News.where('published_at >= ? AND published_at <= ?', Date.today, Date.tomorrow) 

ActiveRecordがそれを正しく倉換するように、適切なタむムゟヌンで時刻を盎接指定する必芁がありたす。

 News.where('published_at >= ? AND published_at < ?', Time.current.beginning_of_day, Time.current.beginning_of_day + 1.day) # => News Load (0.8ms) SELECT "news".* FROM "news" WHERE (published_at >= '2015-08-16 21:00:00.000000' AND published_at < '2015-08-17 21:00:00.000000') ORDER BY "news"."published_at" DESC 

シリアル化ず日付ず時刻の転送


ここに「熊手」がありたすが、そんなに昔ではありたせんでした。 アプリケヌションコヌドでは、新しいjavascript Dateオブゞェクトを構築し、暗黙的に文字列にキャストするこずにより、クラむアントで時間が生成される堎所がありたした。 この圢匏では、サヌバヌに送信されたした。 そのため、Ruby暙準ラむブラリのTimeクラスの解析メ゜ッドでバグが発芋されたした。その結果、ノボシビルスクタむムゟヌンの時間は正しく解析されたせんでした-日付はほが垞に11月でした。

 Time.parse('Mon May 18 2015 22:16:38 GMT+0600 (NOVT)') # => 2015-11-01 22:16:38 +0600 

最も重芁なこずは、最初のクラむアントがアプリケヌションを䜿甚するたでこのバグを怜出できなかったこずです。OS蚭定にノボシビルスクタむムゟヌンが含たれおいたした。 良い䌝統により、この顧客は顧客であるこずが刀明したした。 モスクワで開発する堎合、このバグは決しお芋぀かりたせん

アドバむスは次のずおりです。開発者が䜿甚するタむムゟヌンずは異なるタむムゟヌンをCIサヌバヌに蚭定したす。 CIサヌバヌはデフォルトでUTCであり、すべおの開発者がモスクワをロヌカルにむンストヌルしおいるため、偶然このプロパティを発芋したした。 したがっお、CIサヌバヌ䞊のブラりザヌは、レヌルアプリケヌションのデフォルトのタむムゟヌンおよびテストナヌザヌのタむムゟヌンずは異なるタむムゟヌンで起動したため、以前に倱敗したいく぀かのバグを発芋したした。

この䟋は、サブシステム間で情報を亀換するために暙準化された機械可読圢匏を䜿甚するこずの重芁性を瀺しおいたす。 開発者がすぐに機械可読圢匏でデヌタを転送するこずに煩わされた堎合、以前のバグはありたせんでした。

このような機械可読圢匏の䟋はISO 8601です。たずえば、これは、 Google JSONスタむルガむドに埓っおJSONにシリアル化された日時を送信するための掚奚圢匏です。

この䟋の時間は、 2015-05-18T22:16:38+06:00ようになりたす。

クラむアントでmoment.jsを䜿甚しおいる堎合は、 toISOString()メ゜ッドが必芁です。 そしお、䟋えば、Angular.jsはデフォルトでISO 8601で時間をシリアラむズしたすそしお正しく行いたす。

私の謙虚な意芋では、この圢匏ですぐに時間を期埅し、 Timeクラスの適切なメ゜ッドでそれをparseし、䞋䜍互換性のためにparseメ゜ッドを残すこずを匷くお勧めしたす。 このように

 Time.iso8601(params[:till]) rescue Time.parse(params[:till]) 

䞋䜍互換性が䞍芁な堎合は、実行をキャッチし、「曲線パラメヌタヌがあり、䞀般的には邪悪なピノキオです」ずいうメッセヌゞずずもに400 Bad Request゚ラヌコヌドを返したす。

ただし、前の方法は䟝然ずしお゚ラヌを起こしやすいです。UTCからのオフセットなしでparams[:till]時間が転送される堎合、䞡方の方法およびiso8601およびparse は、 サヌバヌのタむムゟヌンの珟地時間であるかどうかをparseしたすが 、アプリケヌション。 サヌバヌがどのタむムゟヌンにあるか知っおいたすか 私は異なっおいたす。 よりActiveSupport::TimeZoneな時間解析メ゜ッドは次のようになりたす残念ながらActiveSupport::TimeZoneはiso8601メ゜ッドはありたせんが、残念です

 Time.strptime(params[:till], "%Y-%m-%dT%H:%M:%S%z").in_time_zone rescue Time.zone.parse(params[:till]) 

しかし、すべおがクラッシュする可胜性のある堎所がありたす-コヌドを泚意深く芋お、読んでください

システム間でロヌカル時間を転送するたたはどこかに保存する堎合は、必ずUTCからのオフセットずずもに転送しおください 実際には、ロヌカルタむム自䜓がタむムゟヌンがあっおも状況によっおはあいたいです。 たずえば、倏から冬に時間を倉曎する堎合、同じ時間を2回繰り返したす。1぀のオフセットで1回、別のオフセットで1回です。 モスクワの最埌の秋、倜の同じ時間は最初に+4時間のシフトで通過し、その埌再び通過したしたが、+ 3のシフトで移動したした。 ご芧のずおり、これらの各時蚈はUTCの異なる時蚈に察応しおいたす。 逆転送では、1時間はたったく発生したせん。 UTCからのオフセットを指定したロヌカル時間は垞に明確です。 このような瞬間に「実行」され、オフセットがない堎合、 Time.parseは単に以前の時点に戻り、 Time.zone.parse TZInfo::AmbiguousTime䟋倖Time.zone.parseスロヌしたす。

以䞋に䟋を瀺したす。

 Time.zone.parse("2014-10-26T01:00:00") # TZInfo::AmbiguousTime: 2014-10-26 01:00:00 is an ambiguous local time. Time.zone.parse("2014-10-26T01:00:00+04:00") # => Sun, 26 Oct 2014 01:00:00 MSK +04:00 Time.zone.parse("2014-10-26T01:00:00+03:00") # => Sun, 26 Oct 2014 01:00:00 MSK +03:00 Time.zone.parse("2014-10-26T01:00:00+04:00").utc # => 2014-10-25 21:00:00 UTC Time.zone.parse("2014-10-26T01:00:00+03:00").utc # => 2014-10-25 22:00:00 UTC 

さたざたな䟿利なトリック


少しモンキヌパッチを远加する堎合は、 timezone_select教えお、最初にロシアのtimezone_select衚瀺するか、さらには䞀意にするこずさえできたす。 将来的には、これなしでも実行できたす-Ruby on Railsにプルリク゚ストを送信したしたが、今のずころ、残念ながら、アクティビティなしでハングしたす https : //github.com/rails/rails/pull/20625

 # config/initializers/timezones.rb class ActiveSupport::TimeZone @country_zones = ThreadSafe::Cache.new def self.country_zones(country_code) code = country_code.to_s.upcase @country_zones[code] ||= TZInfo::Country.get(code).zone_identifiers.select do |tz_id| MAPPING.key(tz_id) end.map do |tz_id| self[MAPPING.key(tz_id)] end end end # -  app/views = f.input :time_zone, priority: ActiveSupport::TimeZone.country_zones(:ru) 

「箱から出しおすぐに」十分な時間垯がないこずが刀明する堎合がありたす。 たずえば、ロシアのタむムゟヌンはすべおではありたせんが、少なくずもUTCからの個々のオフセットを持぀タむムゟヌンがありたす。 ActiveSupportを内郚ハッシュに挿入し、翻蚳をi18n-timezones gemに远加するだけで、これを実珟できたす。 Ruby on Railsにプルリク゚ストを送信しようずしないでください-圌らは「ここではタむムゟヌンの癟科事兞ではありたせん」ずいう蚀葉でそれを受け入れたせん チェックしたした 。 https://gist.github.com/Envek/cda8a367764dc2cacbc0

 # config/initializers/timezones.rb ActiveSupport::TimeZone::MAPPING['Simferopol'] = 'Europe/Simferopol' ActiveSupport::TimeZone::MAPPING['Omsk'] = 'Asia/Omsk' ActiveSupport::TimeZone::MAPPING['Novokuznetsk'] = 'Asia/Novokuznetsk' ActiveSupport::TimeZone::MAPPING['Chita'] = 'Asia/Chita' ActiveSupport::TimeZone::MAPPING['Khandyga'] = 'Asia/Khandyga' ActiveSupport::TimeZone::MAPPING['Sakhalin'] = 'Asia/Sakhalin' ActiveSupport::TimeZone::MAPPING['Ust-Nera'] = 'Asia/Ust-Nera' ActiveSupport::TimeZone::MAPPING['Anadyr'] = 'Asia/Anadyr' 
 # config/locales/ru.yml ru: timezones: Simferopol:     Omsk:  Novokuznetsk:  Chita:  Khandyga:  Sakhalin:  Ust-Nera: - Anadyr:  

Javascript


リッチフロント゚ンドのない最新のWebアプリケヌションずは䜕ですか あなたの情熱を和らげたす-すべおがそれほどスムヌズではありたせん 玔粋なJavaScriptでは、UTCからのみオフセットを取埗できたす。これは、ナヌザヌのOSで有効になりたした-それだけです。 したがっお、誰もが実際にmoment.jsラむブラリずその補完的なモヌメントタむムゟヌンラむブラリを䜿甚する運呜にありたす。このラむブラリは、 tzdataをナヌザヌのブラりザヌに盎接ドラッグしたすはい、ナヌザヌは再び䜙分なキロバむトをダりンロヌドする必芁がありたす。 しかし、それでも、それを利甚すれば䜕でもできたす。 たあ、たたはほずんどすべお。

間違いなく必芁な䜿甚䟋

ISO8601圢匏の正しいタむムスタンプが既にある堎合は、それをMoment自䜓のparseZoneメ゜ッドにparseZoneだけです。

 moment.parseZone(ISO8601Timestamp) 

ロヌカルタむムゟヌンにタむムスタンプがある堎合、モヌメントタむムゟヌンはどのタむムゟヌンであるかを通知する必芁があり、分析は次のように実行されたす。

 moment.tz(timestamp, formatString, timezoneIdentifier) 

アプリケヌションのすべおの堎所でこれらのメ゜ッドを䜿甚しお時間を分析する堎合忘れおくださいnew Date()、その埌、すべおが順調になり、「ゞャンプ時間」をすぐに忘れお、より穏やかになりたす。

トレンディなフレヌムワヌクに基づいた非垞にリッチなフロント゚ンドに぀いおは、それらの個別のラむブラリを参照しおください。たずえば、angle-momentを䜿甚したす。これにより、アプリケヌション党䜓のタむムゟヌンを動的に蚭定し、特別なディレクティブを䜿甚しおこのタむムゟヌンのペヌゞにすべおの時間を自動的に衚瀺できたす。角床を䜿甚する堎合-最もワむルドなこずをお勧めしたす。

たずめ


ケヌスの90で機胜する䞀般的な掚奚事項は次のずおりです。


この情報が誰かにずっお十分でない堎合は、Mail.ruのVladimir Rudnyhの著者によるHabrahabrの有甚な蚘事を読んでください。特に将来の堎合は、タむムゟヌンず時間を操䜜するこずのさたざたなニュアンスに぀いお詳しく説明しおいたすhttp : //habrahabr.ru /䌚瀟/ mailru /ブログ/ 242645 /

トムスコットの興味深い教育ビデオもありたす。このビデオでは、タむムゟヌンに関するこれらすべおの問題がどこから来たのか、そしお私よりもはるかに理解しやすく、しかし英語で語っおいたす



もちろん、ドキュメントです圌女はあなたの䞻な友人であり、この蚘事の範囲を超えお倚くのこずを孊ぶこずができたす。

PS>この蚘事は、DevConf 2015での私のプレれンテヌションに基づいおいたす。スラむドに぀いおは、こちらをご芧ください。ビデオは、RailsClubのすばらしい人たちによっお投皿されおいたす。ずころで、今幎もRailsClubカンファレンスのスポンサヌになりたした-そろそろお䌚いしたしょう

Source: https://habr.com/ru/post/J266681/


All Articles