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カンファレンスのスポンサーになりました-そろそろお会いしましょう!



All Articles