私が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
)です:
- 指定されたタイムゾーンの現在の時刻を返す
now
メソッド。
Time.zone.now # => Sun, 16 Aug 2015 22:47:28 MSK +03:00
-
parse
メソッドは、Time
クラスのparse
メソッドと同様に、文字列を時間とともにTime
クラスのオブジェクトにparse
ますが、同時にこのオブジェクトのタイムゾーンにすぐに変換します。 UTCからのオフセットが行に示されていない場合、このメソッドは、このタイムゾーンの現地時刻が行に示されると同時に決定します。
ActiveSupport::TimeZone['Novosibirsk'].parse('2015-06-19T12:13:14') # => Fri, 19 Jun 2015 12:13:14 NOVT +06:00
-
at
メソッドは、ご存知のように常にUTCであるUnixタイムスタンプ(1970年1月1日からの秒数)をこのタイムゾーンのTime
型のオブジェクトに変換します。
Time.zone.at(1234567890) #=> Sat, 14 Feb 2009 02:31:30 MSK +03:00
-
local
メソッド。これにより、個々のコンポーネント(年、月、日、時間など)から適切なタイムゾーンの時間をプログラムで構築できます。
ActiveSupport::TimeZone['Yakutsk'].local(2015, 6, 19, 12, 13, 14) # => Fri, 19 Jun 2015 12:13:14 YAKT +09:00
ActiveSupport::TimeZone
クラスは、
Time
クラスのオブジェクトを使用した操作でも積極的に使用されており、次のような便利なメソッドがいくつか追加されています。
-
Time.zone
クラスのメソッドは、アプリケーション全体で現在アクティブなタイムゾーンを表すActiveSupport::TimeZone
クラスのオブジェクトを返します(変更可能です)。
- また、
Time.zone_default
クラスのメソッドは、config/application.rb
ファイルで指定したタイムゾーンを返します。
-
with_zone
メソッドを使用with_zone
と、渡されたブロックで実行されるすべてのコードの現在のタイムゾーンを一時的に変更できwith_zone
。
- さて、
Time#in_time_zone
のメソッドを使用Time#in_time_zone
と、既存のオブジェクトのタイムゾーンを変更できTime#in_time_zone
(タイプActiveSupport::TimeWithZone
オブジェクトを返します)。
Time.parse('2015-06-19T12:50:00').in_time_zone('Asia/Tokyo') # => Fri, 19 Jun 2015 18:50:00 JST +09:00
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%で機能する一般的な推奨事項は次のとおりです。
- (. . ) UTC.
- . , , UTC.
- : , UTC . , - «» - .
- , .
- , , - , , .
- , — , .
- , — , , UTC .
- , — NTP
tzdata
(,tzdata
).
この情報が誰かにとって十分でない場合は、Mail.ruのVladimir Rudnyhの著者によるHabrahabrの有用な記事を読んでください。特に将来の場合は、タイムゾーンと時間を操作することのさまざまなニュアンスについて詳しく説明しています:http : //habrahabr.ru /会社/ mailru /ブログ/ 242645 /
トムスコットの興味深い教育ビデオもあります。このビデオでは、タイムゾーンに関するこれらすべての問題がどこから来たのか、そして私よりもはるかに理解しやすく、しかし英語で語っています:
もちろん、ドキュメントです!彼女はあなたの主な友人であり、この記事の範囲を超えて多くのことを学ぶことができます。
- http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html
- http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html
- http://api.rubyonrails.org/classes/Time.html
PS>この記事は、DevConf 2015での私のプレゼンテーションに基づいています。スライドについては、こちらをご覧ください。ビデオは、RailsClubのすばらしい人たちによって投稿されています。ところで、今年もRailsClubカンファレンスのスポンサーになりました-そろそろお会いしましょう!