Javaと時間:パート1

8年前、私は世界中のユーザーのニーズに応え、それらの行動を調整することになっているサービスの設計と開発に参加しました。 プロジェクトに取り組んでいる間、私は非常に多くの場合、作業の多くの重要な側面が時間の経過とともに単に無視されることに気付きました。 サービスがローカルで特定の地域でのみ使用されている場合、またはユーザーが自然にほとんど相互作用しない地理的クラスターに分割されている場合、これは実際にはそれほど重要ではありません。 ただし、サービスが世界中のユーザーを結び付けている場合、仕事の原則を明確に理解しないと、時間をかけてやることはできません。 一般的なイベント(会議など)が厳密に定義された時間に始まり、ユーザーがそれを頼りにするサービスを想像してください。 いつそれらを表示するか、どの時点で通知に悩まされるべきか、彼らの誕生日は何であるか、そしていつあなたが人を祝福することができるか-記事でこれを理解しようとします。



この記事は、深みがある、または学術的であると主張していません。 これは、経験を体系化し、開発者の注意をあまり明白でない側面に向けようとする試みです。



新しいDate Time APIが JDK8に登場しました。このAPIには 、多くの新しい便利なクラスが登場しましたが優れたサードパーティライブラリについては言及しません。 これは、別の記事に値する別の大きなトピックです。 さらに、既に述べたように、特定の技術についてではなく、全体としての作業の原則についてお話ししようとしています。この点で、新しいAPIは基本的に古いものと変わりません。

時間軸



遠くから始めましょう。 とても遠い。 時間軸を描きます。



ここからさまざまな質問が始まります。 時間は常に同じ方向に進みますか? 均等に行きますか? 連続ですか? 単位ベクトルとして何を取りますか? この軸はすべて同じですか? 空間内の位置に依存しますか? 動きの速さから? 最小測定長は何ですか?

実際、ここで私は質問をする準備ができているだけで、決して答えません。 時間がないと信じられていますが 、私はこの問題について議論する準備もできていません。

しかし、すでに解決されたポイントがあります。

測定単位についてはすべて明らかです。明確かつ明確に指定されています。

モスクワからワシントンまでの距離は約7840000メートルで、光はこの距離を少なくとも0.026秒で地表上を移動します。これは非常に長い時間です。 ウラジオストクでユーザーアカウントを作成するリクエストは、しばらくしてからモスクワサーバーで処理されます。 したがって、発生したイベントに関する情報はすぐには利用できず、空間内のポイント間の距離に依存します。

さらに、 GPSのような非常に普通の近地球テクノロジーでも、時間の速度自体はオブジェクトの速度に依存します。

現在の標準Java時間処理ライブラリは、相対論的効果がなく、誰もが光速に近い速度で動くことはないと考えており、時間軸はすべて(少なくとも同じ惑星で)同じであり、私たち全員に適しています。 おそらく後で、新しいJava Date Time APIがJDK#6543に実装されます。これにより、 Millennium Falcon内部オフィスシステム用のサービスを作成でき、その速度と近くのモルホールの有無が考慮されます。

ここで、時間軸上の特定の瞬間をマークします。 たとえば、今はドットボタンをクリックします。 (クリック)



次に、このボタンを押した正確なタイミングを説明できる方法を考え出す必要があります。 これを行う最も簡単な方法は、私たち全員に共通の時点を指定することです。この時点から、常に時間サンプルをカウントします。 この瞬間が示されている場合( その同じ瞬間 )、この一般的な瞬間からのサンプルの数を送信することができます。ボタンが押された時間と、値を取得したときの現在の時間との関係を理解できます。

原始的な物理世界では、同じ砂時計に出会って同時に走ることができました。 その後、落ち着いてあなたのビジネスを調べ、イベントの時間をウォッチインスタンスの砂柱の高さの形で報告します(おそらく、ウォッチは非常に大きくてかさばるはずです)。



私たちが使用する同じモーメントは 、次元でもありえますが、より一般的で重要なイベントに関してです。 私たちの場合、これはJavaで使用されるUnixタイムシステム (数値0)で同じ瞬間に起こります-これは、1970年1月1日のR.Khから00:00:00とラベル付けされた時点です。 UTCはすでに別のスケール- グレゴリオ暦です。

Javaはそれと何の関係があるのか



Javaの時間軸に時刻を設定するために、クラスjava.util.Dateがあります。 一般に、java.util.Dateは、最も初期のバージョンからのJavaの恥です。 まず、彼の名前は本質を反映していません。 そして第二に、彼は可変です。 ただし、 その同じ瞬間からのミリ秒数を格納するlong型の内部フィールドの単純なラッパーとして認識すると、人生はより単純になります-それ以上は何もありません。 他のすべてのメソッドは廃止されたものとしてマークされており、どのような場合でもそれらを使用する必要はありません。 java.utl.Dateは(本質的に)ミリ秒単位の単純な長いUnix時間の数値と同一であることを覚えておいてください。

Date moment = new Date(1451665447567L); //    Unix-time  -- moment.getTime(); //    Unix-time  --. 


少し考えてみると、どの言語でも時間表現の精度に制限があることが明らかになります。 したがって、java.util.Dateは(他の類似タイプと同様に)時間軸上のポイントではなく、セグメントです。 私たちの場合、ミリ秒の持続時間のセグメント。 しかし、実用的な観点からは、このような精度は私たちに適しているため、これをポイントと呼び続けます。

Javaの表現は最初から64ビットなので、確かに十分なものがあります:
私たちに十分
 Date theEnd = new Date(Long.MAX_VALUE); DateFormat dateFormat = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.LONG, SimpleDateFormat.LONG); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); String text = dateFormat.format(theEnd); System.out.println(text); # August 17, 292278994 7:12:55 AM UTC 



個々のカレンダーフィールド(年、月、日、時間、分、秒など)の読み取り/設定/変更など、時間に関するさまざまな操作には、クラスjava.util.Calendarがあります。 また、罪がないわけではありません-操作中、月は0から始まり(定数を使用する方が良い)、日は1から始まることに注意してください。
java.util.Calendar
  @Test public void testSunday() throws Exception { Calendar calendar = Calendar.getInstance(); calendar.setTimeZone(TimeZone.getTimeZone("UTC")); calendar.set(2016, Calendar.JANUARY, 5, 12, 30, 0); calendar.add(Calendar.DAY_OF_YEAR, -2); Assert.assertEquals(Calendar.SUNDAY, calendar.get(Calendar.DAY_OF_WEEK)); } 



java.util.Calendarのもう一つの濁った瞬間は、その中に完全な日付(yyyy、MM、dd、HH、mm、ss)を設定すると、ミリ秒の数は0にリセットされず、前の設定された瞬間からのミリ秒の数に等しいままです(カレンダーが変更されていない場合は現在の時刻)。 したがって、タスクの条件に従ってミリ秒単位で0が必要な場合、このフィールドはもう1回追加の呼び出しでリセットする必要があります。
java.util.Calendar
  @Test public void testCalendarMs() throws Exception { TimeZone tz = TimeZone.getTimeZone("Europe/Moscow"); Calendar calendar = Calendar.getInstance(tz); calendar.setLenient(false); calendar.set(2016, Calendar.APRIL, 20, 12, 0, 0); System.out.println(calendar.getTimeInMillis()); calendar.set(Calendar.MILLISECOND, 0); System.out.println(calendar.getTimeInMillis()); } 

 1461142800808
 1461142800000



最初の番号は、呼び出しの時間に応じて、最初の3桁になります。 この動作は、テストでは非常に重要です。

タイムスタンプを軸上のポイントに、またはその逆に変換するために、クラスjava.text.DateFormatとその子孫があります。
java.text.DateFormat
  @Test public void testFormat() throws Exception { TimeZone tz = TimeZone.getTimeZone("Europe/Moscow"); DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); dateFormat.setLenient(false); dateFormat.setTimeZone(tz); Date moment = dateFormat.parse("2005-03-27 01:30:00"); Assert.assertEquals("2005-03-27 01:30:00", dateFormat.format(moment)); } 



java.text.DateFormatおよびjava.util.Calendarについては、次のことを明確に言う必要があります。


(古いIPAには)さらにいくつかのクラスがあります:
java.sql.Timestamp-データベースのTIMESTAMP型を操作するためのナノ秒精度のjava.util.Dateの拡張(サブクラス)
java.sql.Date-データベースのDATE型を操作するためのjava.util.Dateの拡張(サブクラス)。
java.sql.Time-データベースのTIME型を操作するためのjava.util.Dateの拡張(サブクラス)。


さらに、任意の時点を、ミリ秒単位の通常のUnix時間の長い値として保存できます。

タイムゾーン



世界中(またはロシア全体)に多くのオフィスを持つグローバル企業で働いた経験のある人なら誰でも、「会議は2016年1月1日14:00:00に開催されます」というフレーズに含まれる情報は事実上役に立たないことを知っています。 「14:00」というマークは、時間軸上の特定のポイントに対応するものではなく、複数のポイントに対応するものです。 全員が同時にビデオチャットルームに集まるようにするために、会議の主催者は別の何か、つまり「14:00」マークを解釈するタイムゾーンを指定する必要があります。 多くの場合、タイムゾーンは本部本部によって暗示されます(「モスクワ時間に従って作業する」)。そうでない場合、デフォルトのタイムゾーンがまったく暗示されない場合は、「2016年1月1日14:00に」明示的に設定する必要があります:00 MSK ''-この場合、時間軸上のポイントは完全に明確に設定され、全​​員が同時に会議に集まります。

HH:MM:CC形式のタイムスタンプを使用した操作のあいまいさを排除するには、タイムスタンプの表示時と入力時の両方でタイムゾーンを指定する必要があります。

何らかの方法で暗黙的に暗示できる場合、タイムゾーンを明示的に指定することはできません。


通常、サービスのタイムゾーン(デフォルト)とサーバーのタイムゾーン(デフォルト)は同じではないことに注意してください。 BIOS、システム、アプリケーション、およびユーティリティは、たとえば、UTCタイムゾーンで動作できますが、同時に、サービスのタイムゾーンはモスクワのタイムゾーンになります。 もちろん、一致する場合ははるかに簡単ですが、この場合、管理者はサーバーにタイムゾーンを設定し、プログラマーはそれらについてまったく考えません。 これがちょうどあなたの場合であるなら、あなたはこれ以上読むことができません。

同じタイムスタンプをさまざまな方法でユーザーに表示できます。誰もが最も使い慣れたタイムゾーンを使用します。 たとえば、次のタイムスタンプは、時間軸上の同じ時点を指します。
 Fri Jan 1 16:29:00 MSK 2016 Fri Jan 1 13:29:00 UTC 2016 Fri Jan 1 14:29:00 CET 2016 Fri Jan 1 21:29:00 SGT 2016 Fri Jan 1 22:29:00 JST 2016 


タイムゾーンに関するウィキペディア

Javaでは、タイムゾーン情報はjava.util.TimeZoneクラスで表されます。

タイムゾーンはオフセットではないと言わなければなりません。 これは、GMT + 3がヨーロッパ/モスクワであると言っているわけではありません。 しかし、2016年全体で、タイムゾーンヨーロッパ/モスクワはGMT + 3オフセットに対応すると言えます。 タイムゾーンは、履歴期間全体の変位の完全な履歴、およびさまざまな過去の瞬間の変位を正確に計算し、正しい時間計算を行うことができる他のデータです。

タイムゾーンを調べてみましょう-最初にヨーロッパ/モスクワを見てみましょう:
ヨーロッパ/モスクワ
  @Test public void testTzMoscow() throws Exception { TimeZone tz = TimeZone.getTimeZone("Europe/Moscow"); System.out.println(tz.getRawOffset()); System.out.println(tz.getOffset(System.currentTimeMillis())); System.out.println(tz.useDaylightTime()); System.out.println(tz.getDisplayName(false, TimeZone.LONG, Locale.ENGLISH)); System.out.println(tz.getDisplayName(false, TimeZone.SHORT, Locale.ENGLISH)); System.out.println(tz.getDisplayName(true, TimeZone.LONG, Locale.ENGLISH)); System.out.println(tz.getDisplayName(true, TimeZone.SHORT, Locale.ENGLISH)); System.out.println(tz.getDisplayName(false, TimeZone.LONG, Locale.FRENCH)); System.out.println(tz.getDisplayName(false, TimeZone.SHORT, Locale.FRENCH)); System.out.println(tz.getDisplayName(true, TimeZone.LONG, Locale.FRENCH)); System.out.println(tz.getDisplayName(true, TimeZone.SHORT, Locale.FRENCH)); } 

 10,800,000
 10,800,000
偽
モスクワ標準時
 Msk
モスクワ夏時間
 Msd
 Heure Standard de moscou
 Msk
 Heureavancéede moscou
 Msd



ヨーロッパ/モスクワゾーンでは、現在のベースオフセットがUTCに対して+3時間であり、現時点での合計オフセットも+3時間であることがわかります。 将来、夏時間の矢印の翻訳はありません。 ゾーンには、夏時間と冬時間の別々の名前があります。

パリの時間を見てみましょう。
ヨーロッパ/パリ
  @Test public void testTzParis() throws Exception { TimeZone tz = TimeZone.getTimeZone("Europe/Paris"); System.out.println(tz.getRawOffset()); System.out.println(tz.getOffset(System.currentTimeMillis())); System.out.println(tz.useDaylightTime()); System.out.println(tz.getDisplayName(false, TimeZone.LONG, Locale.ENGLISH)); System.out.println(tz.getDisplayName(false, TimeZone.SHORT, Locale.ENGLISH)); System.out.println(tz.getDisplayName(true, TimeZone.LONG, Locale.ENGLISH)); System.out.println(tz.getDisplayName(true, TimeZone.SHORT, Locale.ENGLISH)); System.out.println(tz.getDisplayName(false, TimeZone.LONG, Locale.FRENCH)); System.out.println(tz.getDisplayName(false, TimeZone.SHORT, Locale.FRENCH)); System.out.println(tz.getDisplayName(true, TimeZone.LONG, Locale.FRENCH)); System.out.println(tz.getDisplayName(true, TimeZone.SHORT, Locale.FRENCH)); } 

 3,600,000
 3,600,000
本当
中央ヨーロッパ時間
セト
中央ヨーロッパ夏時間
セスト
中央ヨーロッパ
セト
ヨーロッパの中央駅
セスト



ベースオフセットはUTCに対して+1時間で、現在の合計オフセットも+1時間です。 ゾーンの将来の夏時間への移行。 冬と夏時間に別々に、2つの名前も割り当てられます。

次に、ゾーン「GMT + 5」を見てみましょう。 これは実際には一時的なゾーンではありません-歴史も夏時間もないし、シフトは一定です。
GMT + 5
  @Test public void testGmt5() throws Exception { TimeZone tz = TimeZone.getTimeZone("GMT+5"); System.out.println(tz.getRawOffset()); System.out.println(tz.getOffset(System.currentTimeMillis())); System.out.println(tz.useDaylightTime()); System.out.println(tz.getDisplayName(false, TimeZone.LONG, Locale.ENGLISH)); System.out.println(tz.getDisplayName(false, TimeZone.SHORT, Locale.ENGLISH)); System.out.println(tz.getDisplayName(true, TimeZone.LONG, Locale.ENGLISH)); System.out.println(tz.getDisplayName(true, TimeZone.SHORT, Locale.ENGLISH)); System.out.println(tz.getDisplayName(false, TimeZone.LONG, Locale.FRENCH)); System.out.println(tz.getDisplayName(false, TimeZone.SHORT, Locale.FRENCH)); System.out.println(tz.getDisplayName(true, TimeZone.LONG, Locale.FRENCH)); System.out.println(tz.getDisplayName(true, TimeZone.SHORT, Locale.FRENCH)); } 

 18,000,000
 18,000,000
偽
 GMT + 05:00
 GMT + 05:00
 GMT + 05:00
 GMT + 05:00
 GMT + 05:00
 GMT + 05:00
 GMT + 05:00
 GMT + 05:00



つまり、バイアスは一定で、GMTと比較して+5時間であり、変化することはありません。



私たちは言葉で説明するには長すぎるものを例で語り続けます。 まず、2005年に「ヨーロッパ/モスクワ」のタイムゾーンで何が起こったのかを見てみましょう。
ヨーロッパ/モスクワ-2005
 $ zdump -v /usr/share/zoneinfo/Europe/Moscow | grep 2005 /usr/share/zoneinfo/Europe/Moscow Sat Mar 26 22:59:59 2005 UT = Sun Mar 27 01:59:59 2005 MSK isdst=0 gmtoff=10800 /usr/share/zoneinfo/Europe/Moscow Sat Mar 26 23:00:00 2005 UT = Sun Mar 27 03:00:00 2005 MSD isdst=1 gmtoff=14400 /usr/share/zoneinfo/Europe/Moscow Sat Oct 29 22:59:59 2005 UT = Sun Oct 30 02:59:59 2005 MSD isdst=1 gmtoff=14400 /usr/share/zoneinfo/Europe/Moscow Sat Oct 29 23:00:00 2005 UT = Sun Oct 30 02:00:00 2005 MSK isdst=0 gmtoff=10800 



さて、手が夏に動き、冬に戻るのがわかります。 タイムスタンプでこれらの時間に何が起こるか見てみましょう。 手始めに-冬時間への移行:
冬時間
  @Test public void testWinterTime() throws Exception { TimeZone tz = TimeZone.getTimeZone("Europe/Moscow"); DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); dateFormat.setLenient(false); dateFormat.setTimeZone(tz); Calendar calendar = Calendar.getInstance(); calendar.setLenient(false); calendar.setTimeZone(TimeZone.getTimeZone("UTC")); calendar.set(2005, Calendar.OCTOBER, 29, 22, 0, 0); for (int i = 0; i < 62; i++) { String mark = dateFormat.format(calendar.getTime()); System.out.printf("%s - %d, %s\n", mark, tz.getOffset(calendar.getTimeInMillis()), tz.inDaylightTime(calendar.getTime())); calendar.add(Calendar.MINUTE, +1); } } 

 2005-10-30 02:00:00 MSD-14400000、true
 2005-10-30 02:01:00 MSD-14400000、true
 ...
 2005-10-30 02:58:00 MSD-14400000、true
 2005-10-30 02:59:00 MSD-14400000、true
 2005-10-30 02:00:00 MSK-10,800,000、false
 2005-10-30 02:01:00 MSK-10,800,000、false



02:59:00 MSD後、矢印は1時間戻り、次のマークはすでに02:00:00 MSK-冬時間です。 タイムゾーンは、夏時間が終了し、オフセットがGMT + 4からGMT + 3に変更されたことも示します。

この例には興味深いニュアンスがあります。ヨーロッパ/モスクワゾーンを使用する場合、02:00:00 MSDマークに対応するカレンダー上のポイントを設定することは絶対に不可能です。ポイントは必要な1時間後の02:00:00 MSKに設定されます。 このポイントを参照ポイントとして設定するには、すべてを設定できるUTCタイムゾーンのサービスに頼る必要があります。 別のオプションは、ヨーロッパ/モスクワゾーンでMSDポイント01:00:00を設定し、時間を追加することです。

現在-夏時間:
夏時間
  @Test public void testSummerTime() { TimeZone tz = TimeZone.getTimeZone("Europe/Moscow"); DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); dateFormat.setLenient(false); dateFormat.setTimeZone(tz); Calendar calendar = Calendar.getInstance(); calendar.setLenient(false); calendar.setTimeZone(TimeZone.getTimeZone("UTC")); calendar.set(2005, Calendar.MARCH, 26, 22, 0, 0); for (int i = 0; i <= 60; i++) { String mark = dateFormat.format(calendar.getTime()); System.out.printf("%s - %d, %s\n", mark, tz.getOffset(calendar.getTimeInMillis()), tz.inDaylightTime(calendar.getTime())); calendar.add(Calendar.MINUTE, +1); } } 

 2005-03-27 01:00:00 MSK-10,800,000、false
 2005-03-27 01:01:00 MSK-10,800,000、false
 ...
 2005-03-27 01:58:00 MSK-10,800,000、false
 2005-03-27 01:59:00 MSK-10,800,000、偽
 2005-03-27 03:00:00 MSD-14400000、true
 2005-03-27 03:00:01 MSD-14400000、true



01:59:00以降、MSKは03:00:00 MSDの直後に続きます。つまり、針が1時間先に移動します。 タイムゾーンは、この時点でオフセットがGMT + 3からGMT + 4に変化することを通知し、夏時間フラグも表示されます。

しかし、「ヨーロッパ/モスクワ」ゾーンで「2005-03-27 02:30:00」というラベルを処理しようとするとどうなりますか?理論的には、そのようなラベルは存在すべきではありませんか?
存在しないラベル
  @Test public void testMissing() throws Exception { TimeZone tz = TimeZone.getTimeZone("Europe/Moscow"); DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); dateFormat.setLenient(false); dateFormat.setTimeZone(tz); Date moment = dateFormat.parse("2005-03-27 02:30:00"); System.out.println(moment); } 

 java.text.ParseException:解析不能な日付: "2005-03-27 02:30:00"



そうです-厳格モードでは例外が発生します。

手が冬時間に切り替わった日の1日の継続時間を計算します。
冬時間の変換
  @Test public void testWinterDay() { TimeZone tz = TimeZone.getTimeZone("Europe/Moscow"); Calendar calendar = Calendar.getInstance(); calendar.setLenient(false); calendar.setTimeZone(tz); calendar.set(2005, Calendar.OCTOBER, 30, 0, 0, 0); Date time1 = calendar.getTime(); calendar.add(Calendar.DAY_OF_YEAR, +1); Date time2 = calendar.getTime(); System.out.println(TimeUnit.MILLISECONDS.toHours(time2.getTime() - time1.getTime())); } 

 25



2005-10-30 00:00:00 MSDから2005-10-31 00:00:00 MSKまで、24時間ではなく25時間が経過しました。

次に、夏時間を確認します。
夏時間
  @Test public void testSummerDay() { TimeZone tz = TimeZone.getTimeZone("Europe/Moscow"); Calendar calendar = Calendar.getInstance(); calendar.setLenient(false); calendar.setTimeZone(tz); calendar.set(2005, Calendar.MARCH, 27, 0, 0, 0); Date time1 = calendar.getTime(); calendar.add(Calendar.DAY_OF_YEAR, +1); Date time2 = calendar.getTime(); System.out.println(TimeUnit.MILLISECONDS.toHours(time2.getTime() - time1.getTime())); } 

 23



2005-03-27 00:00:00 MSKから2005-03-28 00:00:00 MSDまで、24時間ではなく23時間経過しました。

これらの最後の2つの例は、24 * 60 * 60 * 1000ミリ秒を24時間ではなくカレンダー日として追加する人専用です。 夏時間と冬時間の乗り換えはもうないので、今はそのような問題はないと言えるでしょう。 これに私は以下に答えることができます:


java.sql.Time、java.sql.Date



型はそれぞれSQL型のTIMEおよびDATEで動作するように設計されています。 両方の値は時間帯に依存しないことが理解されていますが、残念ながらこれは完全に真実ではありません。 両方のタイプはjava.util.Dateの子孫であるため、日数の解釈はタイムゾーンに依存します。
java.sql型の問題
  @Test public void testSqlTime() throws Exception { //    2015-01-01 01:00:00 MSK Calendar calendar = Calendar.getInstance(); calendar.setTimeZone(TimeZone.getTimeZone("Europe/Moscow")); calendar.setTimeInMillis(0); calendar.set(2015, Calendar.JANUARY, 10, 1, 0, 0); long now = calendar.getTimeInMillis(); //   java.sql.Time java.sql.Time sqlTime = new java.sql.Time(now); java.sql.Date sqlDate = new java.sql.Date(now); //        Europe/London DateFormat timeFormat = new SimpleDateFormat("HH:mm:ss"); timeFormat.setTimeZone(TimeZone.getTimeZone("Europe/London")); Assert.assertEquals("22:00:00", timeFormat.format(sqlTime)); //        Europe/London DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); dateFormat.setTimeZone(TimeZone.getTimeZone("Europe/London")); Assert.assertEquals("2015-01-09", dateFormat.format(sqlDate)); } 



通常、コードは同じタイムゾーンの両方で機能するため、どちらのタイプもビジネスロジックからJDBCドライバーに情報を転送するタスクに対処しますが、シリアル化などのより高度なケースでは、これらのクラスを使用する場合は十分に注意する必要があります

対応するタイプの新しいAPIでは、このような問題は解決されています。

UTC、GMT



ほとんどの人は、GMTとUTCは他のタイムゾーンで形成されるオフセットに関連する特別な指定であることを知っています。 しかし、誰もがUTCとGMTがまったく同じものでないことを知っているわけではありません (正式に)。 つまり、ラベル「2015-12-01 00:00:00 GMT」と「2015-12-01 00:00:00 UTC」は、時間軸上の異なる(近い)ポイントを示しています。

GMTは 、他のオブジェクトに対する地球の位置から天文学的に計算されます。 GMTは、一部の国ではタイムゾーンとして直接使用されます。

地球の回転はランダムに減速するため、地球は常に増加する時間間隔で同じ位置にあります。 したがって、隣接するGMTマーク上の時刻ポイント間の距離(たとえば、「10:00:01」と「10:00:02」)は1 秒と正確に一致しない場合があります。

UTCは GMTの代わり導入され、原子時間で計算されます。 タイムゾーンとして直接使用されることはありません(ディスプレイスメントのサポートとしてのみ)。

UTCでは、タイムスタンプ間の距離(たとえば、「10:00:01」と「10:00:02」)はまったく同じで、厳密には1秒に等しくなります。 地球の自転の遅れとGMTとの累積的な差は、1年(または2秒)の余分な秒、つまりうるう秒を入力することで解決されます。

したがって、GMTとUTCで同じマークのあるポイントの差が1秒を超えることはありません。

彼らは 、UTCがGMTをほぼ完全に置き換え、GMT + 3の形式でオフセット表記を使用することは長らく悪い考えだと書いています。UTC+ 3表記を使用するのは正しいことです。

GMTもUTCも夏時間はありません。

Javaで使用されるUnix-timeは 、UTCまたはGMTに直接対応していないと言わなければなりません。 一方では、Unix時間では、隣接するラベル間の差は常に1秒ですが、他方では、Unix時間ではle秒は想定されていません。

デフォルトのタイムゾーン



出力にタイムゾーンを明示的に表示するか、表示しないか、入力時にタイムゾーンを要求するか、時間どおりの操作中にタイムゾーンを指定するか指定しないかを要求しませんか?これらの操作には暗黙的に一部のタイムゾーンが存在しています。 独自のものを指定しなかった場合、デフォルトのタイムゾーンが使用されます。

デフォルトのタイムゾーンという用語は、上記のテキストですでに何度か言及されています。 なぜなら、この概念がなければ、実際には何も説明できないからです。 タイムスタンプの時間、出力、および入力を伴うすべての操作にはタイムゾーンが必要です。 あなたがそれを指定しないという事実は、それがそこにないことを意味しない-それは単にデフォルトで取られる。

しかし、すべてが再びそれほど単純ではありません-デフォルトでは誰のために、なぜですか?

カーネルから始めましょう。 hwclockマニュアルでは、カーネルにはタイムゾーンの内部概念がありますが、FATファイルシステムドライバーのようないくつかの珍しいモジュールを除いて、ほとんど誰も使用していません。 同じhwclockコマンドを使用して、タイムゾーンの変更についてカーネルに通知できます。

アプリケーションアプリケーションは、いくつかの方法でデフォルトのタイムゾーンを定義します。

まず、Ubuntuのシステム全体のタイムゾーン(それに関する完全な情報)は、ファイル(シンボリックリンク)/ etc / localtimeに保存され、このタイムゾーンの名前はファイル/ etc / timezoneにあります。
 $ cat /etc/timezone Europe/Moscow $ file /etc/localtime /etc/localtime: timezone data, version 2, 15 gmt time flags, 15 std time flags, no leap seconds, 77 transition times, 15 abbreviation chars $ zdump -v /etc/localtime | head -n 10 /etc/localtime -9223372036854775808 = NULL /etc/localtime -9223372036854689408 = NULL /etc/localtime Wed Dec 31 21:29:42 1879 UT = Wed Dec 31 23:59:59 1879 LMT isdst=0 gmtoff=9017 /etc/localtime Wed Dec 31 21:29:43 1879 UT = Thu Jan 1 00:00:00 1880 MMT isdst=0 gmtoff=9017 /etc/localtime Sun Jul 2 21:29:42 1916 UT = Sun Jul 2 23:59:59 1916 MMT isdst=0 gmtoff=9017 /etc/localtime Sun Jul 2 21:29:43 1916 UT = Mon Jul 3 00:01:02 1916 MMT isdst=0 gmtoff=9079 /etc/localtime Sun Jul 1 20:28:40 1917 UT = Sun Jul 1 22:59:59 1917 MMT isdst=0 gmtoff=9079 /etc/localtime Sun Jul 1 20:28:41 1917 UT = Mon Jul 2 00:00:00 1917 MST isdst=1 gmtoff=12679 /etc/localtime Thu Dec 27 20:28:40 1917 UT = Thu Dec 27 23:59:59 1917 MST isdst=1 gmtoff=12679 /etc/localtime Thu Dec 27 20:28:41 1917 UT = Thu Dec 27 23:00:00 1917 MMT isdst=0 gmtoff=9079 


Ubuntuの場合、ディストリビューション用の特別なコマンド使用してシステムのタイムゾーンを設定できます
 $ dpkg-reconfigure tzdata 


ポライトtzselectユーティリティもあります。
 $ tzselect Please identify a location so that time zone rules can be set correctly. Please select a continent, ocean, "coord", or "TZ". 1) Africa 2) Americas 3) Antarctica 4) Arctic Ocean 5) Asia 6) Atlantic Ocean 7) Australia 8) Europe 9) Indian Ocean 10) Pacific Ocean 11) coord - I want to use geographical coordinates. 12) TZ - I want to specify the time zone using the Posix TZ format. #? 


タイムゾーンを指定する2番目の方法は、環境変数TZです。この環境変数では、各プログラムやユーザーごとにタイムゾーンの識別子を個別に指定できます。

 $ echo $TZ $ date Wed Dec 30 20:18:18 MSK 2015 $ TZ=UTC date Wed Dec 30 17:18:25 UTC 2015 $ TZ=Europe/London date Wed Dec 30 17:18:35 GMT 2015 $ TZ=Europe/Paris date Wed Dec 30 18:18:40 CET 2015 


一部のプログラムでは、設定および/またはコマンドライン引数で特定のタイムゾーンを要求できます。
 $ date --utc Fri Jan 1 08:34:36 UTC 2016 


ところで、時間なしで現在のタイムゾーンのみを表示するように日付を要求できます:
 $ date +%Z MSK 


しかし、これはlibcでの通常のプログラム用であり、Javaプラットフォーム全体があります。したがって、上記の2つの可能性に加えて、さらに2つあります。

JVMを起動する引数を指定できます。

 $ cat << EOF | scala -Duser.timezone=Europe/Paris print("%s\n%s\n".format(java.util.TimeZone.getDefault().getID(), new java.util.Date())) EOF ... Europe/Paris Wed Dec 30 19:24:00 CET 2015 


また、TimeZone.setDefault(TimeZone timeZone)メソッドを使用して、コードでデフォルトのタイムゾーンを直接設定できます。
 $ cat << EOF | scala > java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("America/Los_Angeles")) > print("%s\n%s\n".format(java.util.TimeZone.getDefault().getID(), new java.util.Date())) > EOF ... America/Los_Angeles Wed Dec 30 10:25:45 PST 2015 


または一度にすべて:
 $ TZ=Europe/London cat << EOF | scala -Duser.timezone=Europe/Paris java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("America/Los_Angeles")) print("%s\n%s\n".format(java.util.TimeZone.getDefault().getID(), new java.util.Date())) EOF ... America/Los_Angeles Wed Dec 30 10:37:28 PST 2015 


タイムゾーンベース



さまざまなや地域の議員や政府は、夏時間を定期的にオン/オフにしたりタイムゾーン間で地域移動したりすることさえなく、アイドル状態ではありません。そのような変更に関するすべての最新情報をシステムに保持することが重要です。そうしないと、時刻が正しく入力および表示されず、睡眠中にSMSが受信され、「+ 2暦日」などの計算が正しくなくなります。

Linuxでは、通常のlibcプログラムは/ usr / share / zoneinfoディレクトリ内のファイルで構成されるタイムゾーンデータベースを使用します。これらのファイルはtzdataパッケージに属し、各ディストリビューションの開発者が積極的に監視しています。このパッケージはタイムリーに更新されますが、問題は覚えていません。最後の手段として、デプロイされたバージョンのLinuxが誰でもサポートされなくなった場合、すべての情報を手動で更新できます。

幸いなことに、これらのファイルのコンテンツの形式や、それらの発生の履歴をここで描く必要はありません-Habréには既にこのトピックに関する優れた記事あるからですWikipediaにも優れた記事があります。

しかし、それほど単純ではありません。

Javaは独自のタイムゾーンデータベースを使用します。そして、原則としてOpenJDKのため、場合は、単純に、簡単にパッケージのtzdata-javaの定期的なパッケージマネージャをアップグレードすることができ、その後、OracleのJDKのいずれかにする必要がありますアップグレードするまったく新しいバージョンに全体JDKをまたは別の使用特別なユーティリティをインストールJDKのタイムゾーンのデータベースを更新します。

ところで、上記のJoda-timeライブラリはtzdataシステムデータベースもJVMデータベースも使用していません-はい、内部タイムゾーンデータベースがもう1つありますが、これも個別の方法で更新する必要があります

pythonの場合、別個のライブラリをインストールする必要があります(そして、忘れずに更新する必要があります)

javascriptにはいくつかのサードパーティライブラリがありますが、少なくともGoogle Closureでサポートされていることを正確に覚えています

一般に、特定のソフトウェアがタイムゾーンを使用して個人用データ​​ベースを使用するというテーマは、定期的に表示されます。たとえば、Thunderbird電子メールクライアントのLightningカレンダーモジュールは、個人のsqliteデータベースをタイムゾーンと共に保存するため、状態の最新の変更に伴い、調整のためにこのデータベースに直接介入する必要がありました。そうでなければ、すべての会議は時間通りに出航しました。

一般に、開発者の大部分は(私のような)偏執病に悩まされておらず、タイムゾーンについて誰も考えておらず、tzdataプラットフォームの基本的な配信に誰も含まれていないと感じています(JVM開発者を除く)。

Androidについて別の言葉を言いたいです。簡単に説明します-Androidタイムゾーンは苦痛です。プラットフォームを開発する際に、別のtzdata更新メカニズムについて、または世界中の議員がひどい変化を伴うという事実については誰も考えませんでした(誰が考えていたでしょうか)。タイムゾーンのあるデータベースは、ファームウェアベンダーが必要とする場合にのみ変更されます。一部のベンダーが6か月後に自分の電話を認識しなくなることを考えると、tzdataは多くのデバイスで更新されることはありません。上級ユーザーは、デバイスの現在のタイムゾーンを、現在の状況に多少応じて別のタイムゾーンに変更します(たとえば、ヨーロッパ/モスクワではなくヨーロッパ/ミンスク)。上級ユーザーはまだヨーロッパ/モスクワ(GMT + 4)に住んでおり、単に矢印を回すだけです。その結果、すべてのプログラムのイベントのタイムスタンプは1時間前に変更されます。もちろん、サードパーティの更新ソリューションをルート化および使用するオプションがありますが、すべてのユーザーにルート電話を強制することはできません。



. . , , , , , .

, , -. — . そして、私たちは皆、私たちのカレンダーが多くの1つであり、おそらく最もよく使用されいるとは考えていませんが、それだけではありませんここで遊ぶことができます

うるう年



うるう年は、通常の年のように365日ではなく、366日である年です。うるう年では、2月-2月29日に1日が追加されます。

うるう年は単純であり、Wikipediaで説明されていることを判断するための式

うるう秒



しかし、余分な秒(調整の秒)があれば、すべてがより複雑になります。プロセスの本質は、地球が絶えずわずかに減速しており、同じ時間のマークに沿った星に対するその位置が絶えず変化していることです。修正しないと、昼と夜の時刻が常に変わります。これを防ぐために、科学者は地球の位置を追跡し、必要な補正を計算して調整計画に追加します。減速プロセスは無秩序であるため、長期的な修正計画を立てることは不可能です。修正の2番目を入力する必要性の判断は、現在の状況に応じて行われます。地球が突然逆に加速した場合に備えて、調整のマイナスの秒を入力することも理論的には可能です。

1秒の調整が存在する場合、UTCの時間は60秒の到来とともに流れることが理解されます。
 23:59:58 23:59:59 23:59:60 # leap second 00:00:00 00:00:01 


Unix時間の概念では、 60秒の秒の概念はありません。「うるう秒を処理しないため、時間の線形表現でもUTCの真の表現でもありません。」

少なくとも何らかの方法でUTCの時間に対応するため時刻を1秒から深夜に戻すにはトリックが使用されます。
 23:59:58 23:59:59 23:59:59 # leap second 00:00:00 00:00:01 


トリックが行われ、ファイルのタイムゾーンのデータに基づいて、カーネルによって自律的に正確なタイムサービスのいずれかによって、または。

これは否定的な結果をもたらすハックです。


Javaでは、常にUnix時間の概念に関連付けられているため、うるう秒の考慮もありません古いAPIにも、新しいAPIにも、Joda-timeライブラリにもありません。この場合、情報自体は、うるう秒のtzdataの塩基程度である、この方法にはJavaDoc java.util.Date#getSecondsは、仮説のJavaマシンフィールド値秒60あるいは61に設定することができるいくつかのもの、まだ存在しないと述べ。

まず、第二を考慮していない、うるう秒のJavaクラスの瞬間であることを確認してください。
古いAPI
  @Test public void testLeapSecond1() throws Exception { TimeZone tz = TimeZone.getTimeZone("UTC"); Calendar calendar = Calendar.getInstance(); calendar.setLenient(false); calendar.setTimeZone(tz); calendar.set(2015, Calendar.JUNE, 30, 23, 59, 0); Date d1 = calendar.getTime(); calendar.set(2015, Calendar.JULY, 1, 0, 1, 0); Date d2 = calendar.getTime(); long elapsed = d2.getTime() - d1.getTime(); System.out.println(TimeUnit.MILLISECONDS.toSeconds(elapsed)); } 

 120



結果は120秒であり、121秒ではありません。

次に、新しいAPIを確認します。
新しいAPI
  @Test public void testLeapSecond2() throws Exception { ZonedDateTime beforeLeap = ZonedDateTime.of(2015, 6, 30, 23, 30, 0, 0, ZoneOffset.UTC); ZonedDateTime afterLeap = ZonedDateTime.of(2015, 7, 1, 0, 30, 0, 0, ZoneOffset.UTC); long elapsed = afterLeap.toInstant().toEpochMilli() - beforeLeap.toInstant().toEpochMilli(); System.out.println(TimeUnit.MILLISECONDS.toSeconds(elapsed)); } 

3600



正確に3600秒、3601である必要があります。常に

何秒の調整があったかを正確に知ることができます最も簡単なことは、wikiページで確認することです

他の方法で確認します。調整の秒に関する情報は、ディレクトリ/ usr / share / zoneinfo / right内のタイムゾーンの複製にあります。
 $ file /usr/share/zoneinfo/right/UTC /usr/share/zoneinfo/right/UTC: timezone data, version 2, 1 gmt time flag, 1 std time flag, 26 leap seconds, no transition times, 1 abbreviation char $ zdump -v /usr/share/zoneinfo/right/UTC | grep '59:60' | wc -l 26 


メインディレクトリ/ usr / share / zoneinfoのタイムゾーンファイルには、調整秒に関する情報は含まれていません。
 $ file /usr/share/zoneinfo/UTC /usr/share/zoneinfo/UTC: timezone data, version 2, 1 gmt time flag, 1 std time flag, no leap seconds, no transition times, 1 abbreviation char $ zdump -v /usr/share/zoneinfo/UTC | grep '59:60' | wc -l 0 


どのように見えても、合計26秒です。

次に、UTC 1970-01-01 00:00:00 UTCと2016-01-01 00:00:00 UTCの間に経過した秒数を計算します。Java(Unix-timeによる)とその他のより正確な方法の2つの方法でカウントします。

最初にJava:
1970-01-01 00:00:00 UTCおよび2016-01-01 00:00:00 UTC-Java
  @Test public void testEpochDiff() throws Exception { ZonedDateTime s = ZonedDateTime.of(1970, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); ZonedDateTime f = ZonedDateTime.of(2016, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); long elapsed = f.toInstant().toEpochMilli() - s.toInstant().toEpochMilli(); System.out.println(TimeUnit.MILLISECONDS.toSeconds(elapsed)); } 

1451606400



1451606400が判明し、再確認しました。
 $ dateutils.ddiff --from-zone "right/UTC" -f '%S' "1970-01-01 00:00:00" "2016-01-01 00:00:00" 1451606400 


すべてが収束します-1451606400秒、今では高精度の武器を設定します:
 $ dateutils.ddiff --from-zone "right/UTC" -f '%rS' "1970-01-01 00:00:00" "2016-01-01 00:00:00" 1451606425 


ここで、現在は1451606425秒です。違いが26秒ではなく25秒である理由は明確ではありませんが、他の正確な計算機はまだ見つかりません。



少なくとも標準Javaを使用して制御エンジンを作成する場合、タイムトラベルには深刻な問題があることが明らかになります。正確な時間を設定すると、ロールバックする必要がある秒数を正確に計算できなくなります。エラーは最大で30分になります。正確な未来への移動は、原則としてまったく不可能です-調整の秒数を事前に事前に決定できないためです。

currentTimeMillis()、nanoTime()



Javaの他のほとんどのプラットフォームと同様に、時間のソースは2つあります。1つ目は共通の時間軸に現在のマークを表示し、2つ目はプロセッサに電力が供給されてからの時間をカウントします。



これらの質問およびその他の質問については、ここで詳しく説明します。


動作時間測定



前の章に基づいて、時間の正しい測定についてたった1つの結論を出すことができるように思われます-java.lang.System#nanoTimeメソッドのみを使用する必要があります。java.lang.System#currentTimeMillisメソッドは、システム時間が変更されると値がスキップされる可能性があるため、適切ではありません。この結論は、両方のメソッドのJavaDocを読むことでも確認できます。

ただし、java.lang.Threadクラスのメソッドを見ると、非常に奇妙なことがわかります。
java.lang.Thread#join(long)
  public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } } 



たとえば、java.util.concurrent.ExecutorServiceではSystem.nanoTimeがすでに使用されているため、これは非常に古いものです。

同じ質問は、さまざまなサードパーティライブラリにも非常に関連しており、System.currentTimeMillis()に基づく期間の測定値が自己記述コードに実装されているかどうかは単純に数えられません。

試験時間



この件に関しては、私は木に広めません-冒頭で述べた8年のプロジェクトで成功した経験についてお話しします。

プロジェクトは非常に責任がありました-お金、ポイント、複雑なビジネスロジック、ロールバック、タイムアウト操作、時間経過後のステータスの変更など。

私とフロントエンドの2人の開発者がいましたが、テスターはまったくいませんでした。サーバーには、IoC、インジェクション、低結合、高凝集の原則のフレームワークがありました。それが開発に必要なものになるというわけではありません。しかし、関連する本を読み始めたら、やめるのが難しくなります。心配したのはテストだけでした。テストを書き始めた開発者ほど無力で無責任で甘やかされているものはありません。しかし、遅かれ早かれ、このゴミに進むことを知っていました。

一般的に、私はすぐに決定的にインターフェースを開始しました:
org.myproject.Chronometer
 public interface Chronometer { Date getCurrentMoment(); long getCurrentMs(); long getCurrentTicks(); } 



このインターフェースは、現在の時間が必要なほぼすべての場所に注入されます。期間追跡クラス(org.myproject.Timer)は、このインターフェイスをコンストラクターに取得します。

このアプローチで最も難しいのは、何が可能か、何が不可能かを思い出すことです。
それは可能であり、不可能です
 #   user.setCreated(new Date()); user.setModified(new Date()); #   Date now = chronometer.getCurrentMoment(); user.setCreated(now); user.setModified(now); #   Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_YEAR, -3); Date expiration = calendar.getTime(); #   Calendar calendar = Calendar.getInstance(); calendar.setTimeZone(this.operationTimeZone) calendar.setTime(chronometer.getCurrentMoment()); calendar.add(Calendar.DAY_OF_YEAR, -3); Date expiration = calendar.getTime(); 



もちろん、個人のコードでのみそのような部分を完全にトレースできますが、永続性とビジネスロジックを担当するコードにこの正しいアプローチを実装することが重要です。

テストの問題を議論するとき、データベースを忘れないことは不可能です。
廃止されたエントリを削除する
 #   -   DELETE FROM records WHERE created <= DATE_SUB(NOW(), INTERVAL 30 DAY) #    -       DELETE FROM records WHERE created <= DATE_SUB(:now, INTERVAL 30 DAY) #    -       /  DELETE FROM records WHERE created <= :expiration 



このアプローチでは、どこでも正しく実装されていれば、すべての自動テストをいつでも送信できます-すべてのコードは1953年または2312年に簡単に実行できます。
試験例
 //  ""  mockChronometer.setCurrentTime("2120.06.10 15:33:11"); //      ""  Period period = new Period(); period.setStart(TimeUtils.parse("2120.06.01 00:00:00")); period.setEnd(TimeUtils.parse("2120.07.01 00:00:00")); period.setIndex("wdwwddwwdw"); period.setType(PeriodType.MONTH); period.setDescription("efefef"); periodsDao.save(period); // -  -         "2120.06.10 15:33:11". //   ,      . //         -     (  ). checkSomething1(); // ...  2     mockChronometer.setCurrentTime("2120.06.12 15:38:14"); //            checkSomething2(); 



MockChronometerとTimeUtilsの呼び出しでタイムゾーンを設定しないことについて既にモニターに指を向けてkg / amを教えてくれた場合は、次の点に注意してください。現在のタイムゾーンの容認でテストにそれらを残すことは、テストを脆弱にすることを意味します。実際、メソッド引数でゾーンが特に指定されていない場合、デフォルトでは両方のクラスがデフォルトでUTCタイムゾーンで動作します。

Java 8では、新しいDate Time APIがjava.time.Clockインターフェースを導入しました。これはまったく同じ目的のために導入されましたが、一般の人がすでにこれを高く評価しているかどうかわかりません。

別のアプローチは、エージェントを指定して別のJVMでテストを実行することです、System.nanoTime()およびSystem.currentTimeMillis()への呼び出しをインターセプトするためのコードインストルメンテーションを生成します。私はこのアプローチを試していないので、既製のソリューションの大まかな検索で 提供さませんより堅牢なオプションは、ビルドプロセス中のソースコードの単純な前処理のようです。System.nanoTime()、System.currentTimeMillis()、new Date()、Calendar.getInstance()への呼び出しをそのクラスへの呼び出しに置き換えます。

春のフレームワーク



8年前の春にはLocaleResolverがありましたが、TimezoneResolverはありませんでした(私の意見では、問題に対する一般的な態度を完全に特徴づけています)。独自のキットを作成すると同時に、DispatcherServletのサブクラスを作成する必要がありました。

非常に多数ではなく、コミュニティ(私のものを含む)からの永続的な要求の後、要求のタイムゾーンのスタッフリゾルバがフレームワークの第4バージョンで導入されました。

MVC



別の質問は、コントローラーのタイムゾーンを知って、テンプレートエンジンに正しく設定する方法です。

FreeMarkerには、現在のレンダリング用の特別な設定があります。
 <#setting time_zone="Europe/Moscow"> 


JSPの場合、単一の出力に対してタイムゾーンを個別に指定することも、ブロック全体に対して直ちにタイムゾーンを指定することもできます
 <fmt:formatDate type="both" value="${now}" timeZone="Europe/Moscow"/> <fmt:timeZone value="Europe/Moscow"> <fmt:formatDate type="both" value="${now}"/> </fmt:timeZone> 


Velocity 何かありますが、私は個人的には試していません。

データベースに時間を保存する



データベースに時刻ポイントを格納する最も強力な方法は、値java.util.Date#getTime()を単純な数値の長いUnix時間値としてデータベースに渡すことです。したがって、読み取り時に、コンストラクターを使用してlongをjava.util.Dateに変換します。これは、HibernateまたはRowMapperコンバーターで実行できます。この場合のデータベースは時間について何も知らないため、突然の影響はありません。ラベルの形式で一時ポイントを表示する必要がある場合、たとえばMySQLで、FROM_UNIXTIMEメソッドをいつでも呼び出すことができます。

このメソッドは、データベースおよび/またはストアドプロシージャのクエリに時間のある操作がない場合に適しています。そのような操作がある場合(開発を簡単にするために、もちろん回避する方がよいでしょう)、タイムゾーンなしでそれらが通過できないことを既に認識しています。この場合、変換または入出力操作中にどのタイムゾーンが機能するかを正確に理解する必要があります。


どんな場合でも、このために特別に設計された型で時間を保存する必要はないと言いたいです。直接ストレージを長く使用するオプションは、どういうわけか破るのが非常に難しく(方法がわからない)、この方法はストレージのタイプに依存しないというだけです。

このために特別に設計されたタイプで時間を保存する場合、データベースとそのドライバーの指定された設定で、外出先でタイムゾーンを変更する場合でも、指定された時間値が一貫してこのデータベースのネイティブ管理コンソールに書き込まれ、読み取られ、表示されることを確認する必要があります。

矛盾は、データベースがポイントではなくタイムスタンプを実際に保存できるという事実によって引き起こされる可能性があります。たとえば、MySQLの場合、時間を保存するための2つの標準タイプがあります:TIMESTAMPとDATETIME。

公式文書によると、TIMESTAMPは正確な時点を保存し、モスクワのタイムゾーンでクライアントから値「2015-01-01 12:00:00 MSK」を受信すると、別の場合は「2015-01-01 09:00:00 UTC」を返します。 UTCタイムゾーンのクライアント。これは1つの時点に対応し、本質的に完全に正しいものです。また、DATETIMEタイプでは、MSKクライアントから「2015-01-01 12:00:00 MSK」を受信すると、MySQLサーバーは「2015-01-01 12:00:00 UTC」をUTCクライアントに返します。これは別の時点に対応しますそれ以降の計算はすべて不正確になります。

MySQLを確認してください。まず、すべてを準備します。
DBの起動
 $ sudo docker run --name mysql-time -e MYSQL_ROOT_PASSWORD=root -d mysql/mysql-server:5.7 $ sudo docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 82bb3eebc8bc mysql/mysql-server:5.7 /entrypoint.sh mysq 5 minutes ago Up 5 minutes 3306/tcp mysql-time $ sudo docker exec -it 82bb3eebc8bc bash [root@82bb3eebc8bc /]# TZ=Europe/Moscow mysql -u root -p Enter password: mysql> CREATE DATABASE test; Query OK, 1 row affected (0.00 sec) mysql> use test; Database changed mysql> CREATE TABLE dates (id INTEGER, t1 TIMESTAMP, t2 DATETIME); Query OK, 0 rows affected (0.02 sec) 



セッションで「ヨーロッパ/モスクワ」ゾーンを設定し、レコードを作成します。
ヨーロッパ/モスクワ
 mysql> SET time_zone = 'Europe/Moscow'; Query OK, 0 rows affected (0.00 sec) mysql> SELECT @@session.time_zone; +---------------------+ | @@session.time_zone | +---------------------+ | Europe/Moscow | +---------------------+ 1 row in set (0.00 sec) mysql> INSERT INTO dates VALUES (1, '2015-01-01 12:00:00', '2015-01-01 12:00:00'); Query OK, 1 row affected (0.00 sec) mysql> SELECT * FROM dates WHERE id = 1; +------+---------------------+---------------------+ | id | t1 | t2 | +------+---------------------+---------------------+ | 1 | 2015-01-01 12:00:00 | 2015-01-01 12:00:00 | +------+---------------------+---------------------+ 1 row in set (0.00 sec) 



セッションのタイムゾーンを「UTC」に変更し、レコードを再度読み取ります。
UTC
 mysql> SET time_zone = 'UTC'; Query OK, 0 rows affected (0.00 sec) mysql> SELECT @@session.time_zone; +---------------------+ | @@session.time_zone | +---------------------+ | UTC | +---------------------+ 1 row in set (0.00 sec) mysql> SELECT * FROM dates WHERE id = 1; +------+---------------------+---------------------+ | id | t1 | t2 | +------+---------------------+---------------------+ | 1 | 2015-01-01 09:00:00 | 2015-01-01 12:00:00 | +------+---------------------+---------------------+ 1 row in set (0.00 sec) 



フィールドt1のタイムスタンプが変更されていることがわかりますが、タイムゾーンを変更してもフィールドt2のタイムスタンプは変更されておらず、数値軸上の別のポイントに対応しています。

ほとんどのデータベースには、日付と時刻のみを保存する特定のタイプがあります。このような型は、タイムゾーン(既に知っている)を指定せずに時点に移動することはできませんが、これらの値を欠落部分(それぞれ時刻または日付)で補う必要もあります。実際、たとえば、歴史的出来事の日の単純な結論の場合など、これが必要でない場合、そのようなタイプは時点に持ち込まれないかもしれません。これはラベル(またはその一部)であり、軸上の一時的なポイントではないことを覚えておく必要があります。

多くのデータベースには、タイムポイント自体に加えて、入力されたタイムゾーンに関する追加情報を保存するさまざまなタイプがあります。たとえば、この値の主なタイムゾーンを特定する必要がある場合に役立ちます。

NTP



システムの現地時間が実際の時刻と異なる理由に関係なく、さまざまなプロトコルを使用した正確な時刻配信サービスが助けになります最も一般的なのはNTPプロトコルです

原子時計は非常に高価なので、それらに関連付けられたサーバーに負荷をかけすぎないように、NTP は通常のユーザーの要求を処理するために階層全体を構築します。

クライアントは、異なるサブネットで一度に複数の正確なタイムサービスを定期的にポーリングし、 UDPデータグラムの送受信に費やし時間を補正し、外れ値である値を破棄してから、特別なアルゴリズムでそれらを平均化します

申し立て済み パブリックネットワークで使用すると、可能な同期精度は10ミリ秒に達する可能性があります。

通知



ほとんどのサービスでは、モバイル通知(プッシュとSMS)は、緊急(メッセージ、新しい友情、支払い)と非緊急(プロモーション、広告、オファー、ルールの変更)の2つのクラスに分類できます。ユーザーは、イベントが発生するとすぐに最初のものを受信することを期待する可能性が高く、2番目の場合、イベント自体はそうではない可能性があり、ユーザーにとって都合の良い時間にユーザーを邪魔する方が良いでしょう。たとえば、午前10時から午後20時までのような、デフォルトの快適な時間を考慮することができます。これが重要な場合は、快適な時間を確保して、個別に手動で指定できます。

何らかの方法で、この期間をあるタイムゾーンで解釈する義務があり、これがユーザーのタイムゾーンになることは明らかです。原則として、サービスが特定の地域(たとえば、1つの都市)でのみ機能する場合、これはすべて無視でき、すべてのユーザーのタイムゾーンがサーバーのタイムゾーンと一致すると想定できますが、これは分散サービスには十分ではありません。

したがって、タイムゾーンはバックグラウンドタスク用に保存する必要があります-少なくともユーザーに対して個別に、さらにはユーザーデバイスごとに個別に保存する必要があります。現代の現実では、同じアプリケーションアカウントを複数のデバイスで一度に使用できます。たとえば、モスクワの自宅に常駐している据え置き型のAndroid TVなどです。ユーザーが休暇中にタイで離れるAndroidタブレット上。したがって、両方のデバイスの通知を異なる時間に送信する必要がある可能性があります。ユーザーのタイムゾーンとユーザーにとって快適な時間があるため、いつでもスパムを開始できる時間軸上のポイントを計算できます。

 Date now = chronometer.getCurrentMoment(); TimeZone timeZone = userDeviceRecord.getTimeZone(); Calendar calendarFrom = Calendar.getInstance(timeZone); calendarFrom.setTime(now); calendarFrom.set(Calendar.HOUR_OF_DAY, comfortHourFrom); Calendar calendarTill = Calendar.getInstance(timeZone); calendarTill.setTime(now); calendarTill.set(Calendar.HOUR_OF_DAY, comfortourHourTill); if (now.after(calendarTill.getTime())) { calendarFrom.add(Calendar.DAY_OF_YEAR, +1); } long delayMs = Math.max(calendarFrom.getTime().getTime() - now.getTime(), 0); notificationService.sendNotificationWithDelay(msg, delayMs); 



午前/午後



タイムスタンプの12時間のプレゼンテーションについて話していることをまだ理解していない人はここにいないと思います。

しかし、誰もがこれらの略語の翻訳方法を知っているわけではありません:am(lat。Ante meridiem-文字通り「正午前」)およびpm(lat。Post meridiem文字通り-「正午後」)。

多くの人にとって驚きは、真夜中が午後12時または午前0時ではなく、非常に午前12時であることです。同様に、正午は午前12時でも午後0時でもありませんが、午後12時です。そのような指定では、この形式に慣れている国の居住者でさえ混乱しているので、さまざまなトリックを思いつきます。

リアルタイムクロック



あらゆるアーキテクチャのハードウェアには、バッテリーが接続された特別なチップがあります。これはリアルタイムクロックまたはLinux用語で「ハードウェアクロック」です。



外部条件に関係なく、電子で静かにカチカチ音をたてます:メイン電源がシステムユニットに供給されているかどうか、オペレーティングシステムがインストールされているかどうか、およびアクションの有無。バッテリーのエネルギーがなくなると、それらの進行は中断されますこれは、実際に機器で十分に早く起こります

Linuxで(実際にこのチップにコマンドを送信するために)ハードウェアクロックを管理するには、特別なhwclockコマンドがあります。機器(通常は/ dev / rtc)と直接通信するため、root権限で実行する必要があります。

 $ sudo hwclock Wed 30 Dec 2015 17:59:12 MSK -0.328637 seconds 


このコマンドは、ハードウェアクロックから受信した時刻をシステムのタイムゾーンに自動的に移動します。しかし、出力の最後にこの奇妙なバイアスは何ですか?

実際のところ、Linuxカーネルには、他にも独自のクロック(システム時間)があります。 Linuxカーネルは、カーネルブートの開始時にチップから読み取り値を1回読み取り、その後、割り込み中に自動的にチェックします。ほとんどの場合、これは1つの単純な理由で行われました。各ユーザーリクエストに対してシリアルバス上のハードウェアクロックチップからデータを比較的長い時間読み取るため、ローカルのソフトウェアカウンターのみを保持する方が簡単です。 Linuxカーネルがハードウェアクロックから値を読み取った後、理論的には、後者で任意の操作を実行できます。これは、アプリケーションの時間に影響しません。

したがって、システムでは2時間が並行して実行されます。コアの実際の電子時計とソフトウェア時計です。 hwclockコマンドの出力に表示される違いは必然的に

ありますが、この問題を解決するには2つのオプションがあります(一部の問題ではまったく問題ありません)。

システムがネットワークに接続されていないと仮定して、最初のオプションを検討してください。ハードウェアクロックIron Clockは中断することなく24時間動作するため、平均して毎日同じ量で間違えられます。 hwclockユーティリティは、チップから値を読み取るときに、特別な修正メカニズムによりこれを平準化できます。ユーザーがハードウェアクロックを2回設定すれば、設定の間隔がかなり長くなります。 hwclockユーティリティ自体は、日中にハードウェアクロックがどれだけ間違えたかを計算し、その後、この値を/ etc / adjtimeファイルに保存します。その後、ハードウェアクロックから定期的に値を読み取り、システム時間値を設定できます。ユーティリティ自体は、以前に保存された日次エラー値によって累積エラーを修正します。

2番目のオプションは、カーネルにSystem Timeを定期的に正しくインストールする何らかの方法があることを前提としています。おそらく、これは正確な時刻の外部ソースの一種であり、これを使用してシステム時刻を最新のものにすることができます。これ以降は、カーネルに定期的に(11秒ごとに)システム時間カーネルの正しい値をハードウェア時間チップに落とすように依頼するだけです。

これについては、hwclockチームマニュアルを参照してください

Linuxシステムがハードウェアクロックからの値を2つの方法で解釈できることにも言及する価値があるでしょう。これは、ハードウェアクロックが時間をカウンターyyyy、MM、dd、HH、mm、ss形式で保存するためです。。また、前述のように、タイムゾーンがないと、これらのカウンターを時間軸上のポイントに結び付けることはできません。

実際には、2つのオプションしかありません。UTCまたはローカルタイムゾーン(デフォルトでオペレーティングシステムにインストールされているタイムゾーン)です。

開始するには、BIOSに移動して、メインメニューの時計を見てください。 BIOSと時計のクロックを比較し、一致する場合、BIOSクロックは現地時間です(おめでとうございます-Windowsコンピューターを持っている可能性が高いです)。そして、それらが一致しない場合、BIOSクロックはUTCタイムゾーンに設定されます(または別のオプション-間違ってしまいます)。

Windowsでは、デフォルトで、ハードウェアクロックチップの時間が現地時間と一致する必要があります。また、Linuxはハードウェアクロックのローカル時間とUTC時間の両方を簡単に使用できます(後者が望ましい)。そのため、原則として、システムが2回起動すると、BIOSのクロックはWindows専用の現地時間に従って進み、Linuxはこれに適応します。

Debian / Ubuntuの現在のモードは次のように表示できます。
 $ cat /etc/default/rcS | grep UTC # assume that the BIOS clock is set to UTC time (recommended) UTC=yes 


カーキ時間



現時点でシステム全体の時間とは異なると思わせるプログラムを作成する必要がある場合は、Ubuntu / Debianリポジトリにシステムコールをインターセプトおよび変更するfaketimeユーティリティが既にあります。

 $ date Wed Dec 30 22:53:11 MSK 2015 $ faketime -f '+2y' date Fri Dec 29 22:53:39 MSK 2017 


誕生日パーティー



彼が4月15日に生まれたと誰かが言った場合、毎年この人の誕生日を祝う機会があります。 2001年4月15日に生まれたと彼が言った場合、私たちは彼の年齢についてさらに学ぶ機会を得ます。ただし、いずれの場合も、これは軸上のどの時点にも対応しません。第一に、出生時刻は示されていません。第二に、出生のタイムゾーンは示されていません。理論的には、正確な生年月日が示されている場合、生年月日のタイムゾーンを見つけることができます。

ユーザー自身にお祝いの言葉を送ることができる時点と、彼の誕生日に関する通知を友人に送信する時点をどのように正確に計算できますか?または、次を提案できます。


日本フランス
  @Test public void testBirthday() throws Exception { TimeZone japanTz = TimeZone.getTimeZone("Japan"); Calendar japanCalendar = Calendar.getInstance(japanTz); japanCalendar.setLenient(false); japanCalendar.setTimeInMillis(0); japanCalendar.set(2016, Calendar.APRIL, 15, 9, 0, 0); System.out.println("Japan 2016-04-15 09:00:00: " + japanCalendar.getTimeInMillis()); japanCalendar.set(2016, Calendar.APRIL, 15, 21, 0, 0); System.out.println("Japan 2016-04-16 21:00:00: " + japanCalendar.getTimeInMillis()); TimeZone franceTz = TimeZone.getTimeZone("Europe/France"); Calendar franceCalendar = Calendar.getInstance(franceTz); franceCalendar.setLenient(false); franceCalendar.setTimeInMillis(0); franceCalendar.set(2016, Calendar.APRIL, 14, 9, 0, 0); System.out.println("France 2016-04-14 09:00:00: " + franceCalendar.getTimeInMillis()); franceCalendar.set(2016, Calendar.APRIL, 14, 21, 0, 0); System.out.println("France 2016-04-14 21:00:00: " + franceCalendar.getTimeInMillis()); franceCalendar.set(2016, Calendar.APRIL, 15, 9, 0, 0); System.out.println("France 2016-04-15 09:00:00: " + franceCalendar.getTimeInMillis()); franceCalendar.set(2016, Calendar.APRIL, 15, 21, 0, 0); System.out.println("France 2016-04-15 21:00:00: " + franceCalendar.getTimeInMillis()); } 

日本2016-04-15 09:00:00:1460678400000
日本2016-04-16 21:00:00:1460721600000
フランス2016-04-14 09:00:00:1460624400000
フランス2016-04-14 21:00:00:1460667600000
フランス2016-04-15 09:00:00:1460710800000
フランス2016-04-15 21:00:00:1460754000000





時間t(f)は時間t(u)よりもはるかに長くなる可能性があります。たとえば、ユーザーが日本にいて、友人がヨーロッパにいる場合です。この場合、早朝にヨーロッパの友人に通知が届くまでに、日本のユーザーはすでにDRのお祝いを終えている可能性があります。この場合、1日前に友人に通知する時間を変更し、通知フレーズに「明日」という単語を追加できます。

国境年齢



私にとって非常に興味深い、法的性質のもう1つのポイントがあります。多数の境界年齢があります:大多数の年齢、性的同意の年齢、刑事責任の年齢。

誰かがウラジオストクで01.01.10 00:00:01に生まれたとします。彼はモスクワで2016.01.09 23:59:59に軽犯罪を犯しました。パスポートに誕生日(2000.01.10)を記録し、プロトコルに犯罪の日(2016.01.09)を記録すると、人はまだ16歳になっていないことがわかります。ただし、モスクワでは2016.01.09 23:59:59-ウラジオストク(彼が生まれた場所)では2016.01.10になり、16歳になります。反対の状況では、モスクワで生まれウラジオストクで犯罪を犯した人はすでに16歳になりますが、モスクワの時間に応じて数えると、まだないことがわかります。

司法慣行この事件を排除するために、予想される翌日の00:00:00から権利/責任の活性化が生じる規範が使用されます。つまり、刑事責任は2016.01.11 00:00:00にイベントの場所で発生します。この場合、その人はどこにいても16歳です。

結論





記事の2番目の部分は、 Java 8の新しいDate Time APIについてです。

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


All Articles