※当サイトの記事には、広告・プロモーションが含まれます。

Javaの標準APIで閏年の扱いとかどうなっているのか

xtech.nikkei.com

⇧ 2024年が「閏年」だったということで、システム障害の原因は「閏年」の仕組みに対応できていなかったのが原因ということらしい。

『何をやっとるんだ、君。けっ…けしからんではないか』と、ご立腹される方もおられるかもしれないですが、

xtech.nikkei.com

⇧ 2012年にメジャーどころの「クラウドサービスプロバイダー(CSP:Cloud Service Provider)」である「Microsoft Azure(旧:Windows Azure)」でも、「閏年」の仕組みに対応できずに障害を起こしていたらしいのですよ。

つまり、「閏年」によるシステム障害は技術力のある人間でも起こし得る問題になるということですな。

だって、天下のMicrosoftさんの技術者でも引き起こしているんですから、況んや、その他の技術者においてをや。

で、Microsoftによると、

japan.zdnet.com

⇧「閏年」の年の場合は、366日目になる12月31日にも注意が必要と。

ちなみに、

monoist.itmedia.co.jp

⇧ 1992年にも「閏年」が起因のシステム障害が起きていたという事例があったみたい。

なので、

xtech.nikkei.com

⇧ 上図の「氷山の一角モデル」のように、表沙汰になっていないだけで、「閏年」でシステム障害が起きていた事例は山ほどありそうですな...

そして、よく分からんのだけど、プログラミングの処理の部分しか語られていないのだけど、データベース側とかの日付型は「閏年」で問題を引き起こさないように対応してくれているって考えて良いんだよね?

システム障害の原因について、詳細なナレッジを共有していないからして、同じようなことが繰り返されている気がするんですけどね...

IT業界って、歴史から学べるような文化を作らないのって、何でなんだろう...

まぁ、「独立行政法人情報処理推進機構IPA:Information-technology Promotion Agency, Japan)」が自分たちの失敗についても、事例としてナレッジを共有しようっていう発想が無さそうなんで、この先も変わりそうに無いですかね...

独立行政法人情報処理推進機構(じょうほうしょりすいしんきこう、Information-technology Promotion Agency, Japan、略称: IPA)は、日本IT国家戦略を技術面・人材面から支えるために設立された独立行政法人(中期目標管理法人)。所管官庁は経済産業省

情報処理推進機構 - Wikipedia

⇧『日本IT国家戦略を技術面・人材面から支えるため』と謳っているのにね...誠に遺憾である...

ちなみに、

natgeo.nikkeibp.co.jp

⇧「うるう秒」については、2035年に廃止予定らしい。

xtech.nikkei.com

 うるう秒を巡っては、過去に複数のシステム障害につながった経緯があり、大手IT企業を中心に廃止を求める声が高まっていた。

コンピューター狂わす「うるう秒」が実質廃止へ | 日経クロステック(xTECH)

www.itmedia.co.jp

 IT業界は過去20年以上にわたり、うるう秒の廃止を訴えてきた。例えば米Metaは22年に「うるう秒は1972年には受け入れられる解決策だったかもしれないが、現在では利益よりも害をもたらす危険な行為」と指摘。結局、その年の国際度量衡総会(CGPM)で35年までに新たなUTCを導入し、うるう秒調整を廃止することが決まった。

「うるう秒」はまだ終わっていない 焦点は“新たな協定世界時”の導入時期(1/2 ページ) - ITmedia NEWS

⇧ 廃止した場合にシステムへの影響は無いのかが気になりますが、記事の内容を見る限り「うるう秒」を廃止したとしても全く障害が発生する影響の心配は無いってことなんかな。

とりあえず、IT業界全般的に言えるのだけど、宜しくない点を上げるのであれば、代替案を具体的に提示するまでをセットにして欲しい今日この頃ですかね。

代替案を提示できないんであれば、『今の状況は良くないとは思う、代替案は思いつかんけど』的な発言をして、自分は代替案を持っていません、って情報を開示していくべきだとは思うんよね。

勿論、「代替案」を提案してもらえるのが一番なんだけど。

まぁ、このあたりの「代替案」を提案するような言動が極稀な状態になっているのも、ナレッジを共有しようって文化が無いからなのかもしれませんな...

今現在採用されている日時の概念を整理してみる

閏年」の問題を考える前に、少し脱線します。

改めて考えてみると、1年が365日(「閏年」の場合は366日)というのも、特定の文化圏の人間が勝手に決めているだけで、別に1年180日とかでも良いわけですと。

そうなった場合、誕生日が来るのも早くなって、200歳とかも考えられるわけで、そう考えると年齢とかも人間が作り出してるだけってことになりますな。

生物学的に細胞の劣化は不可逆的ではあるし、永続的に機能するわけでは無いので、時系列の概念は無くならないので、それならば何某かの時系列の状態を表わすのに適したシステムを導入しようじゃないかで、まぁ、天体(太陽や月)の周期に合わせて1年を12ヵ月、1ヵ月30日ほど、1日24時間ということにしようと決めたんだと。

で、Wikipediaさんによると、

24時制(にじゅうよじせい)は、時刻表示の方法の一種である。正子にあたる午前0時から翌日の午前0時までの時刻を、午前0時から経過した時間 (0 - 23) で表現する。

24時制 - Wikipedia

歴史

24時制の起源は、エジプトの天文学のシステム「デカンズ」まで遡り、科学者、天文学者、航海士、そして時計師によって、何世紀にもわたって用いられてきた。有名なプラハの天文時計や、グリニッジのシェパードゲートクロックを含め、24時間表記を使用している時計は多く残っている。

24時制 - Wikipedia

時刻 (じこく)とは、時間の流れにおけるある一点、連続する時間の中のある瞬間

時刻 - Wikipedia

時法

時刻に関する規定を時法(じほう)という。時法は時間に関する「きまり」であり、基本的に一日の時間を分けて、ひとつの「とき」ごとの名称をつける(つまり名称を定める)規定である。

時刻 - Wikipedia

等分方法

古代バビロニア古代エジプト以来の伝統で、時刻の区切りにも基本的に十二進法六十進法が採用されている。古代エジプトなどで日中を12等分していた影響で、現在でも、一日を正午を基準とした「午前」(正午の前)、「午後」(正午の後)の半分に分け、それぞれを0時から12時までの12時間とする「12時制(12時間制)」、あるいは、午前と午後を分けずに一日を0時から24時までとする「24時制(24時間制)」が用いられている。国によって12時制を採用するか、24時制を採用するかの傾向は分かれるが、いずれにしろ「1日は24時間」である。

時刻 - Wikipedia

六十進法の考え方に基づき「時」を60等分したものを「分」と定め、「分」を更に60等分したものを「秒」としている。つまり「1時間は60分」「1分は60秒」と規定している。(なおこれを機械的に再現した機械式時計では、秒針が60「目盛」進むと1周し(その間に並行して)分針が1目盛分進み、分針が60「目盛」進むと1周し(並行して)時針が1目盛む、というメカニズムになっている。)

時刻 - Wikipedia

六十進法(ろくじっしんほう)とは、60 を(てい)とし、底およびそのを基準にして数を表す方法である。

六十進法 - Wikipedia

記数法

紀元前3000年から紀元前2000年の頃から、シュメールおよびその後を継いだバビロニアでは、六十進法が用いられた。シュメール人が六十進法を用いた理由は分かっていない。

六十進法 - Wikipedia

バビロニア数学の六十進法で特徴的なのは、1未満の数を表す際に、早くから小数の概念が存在した事である。ヨーロッパ世界では1未満の数を表すにはエジプト数学より導入した分数エジプト式分数)を用いていたが、計算が面倒であるため、天文学で星の運行の計算をする時など、バビロニアの六十進法が導入された。角度を度数法で表す際の1度未満の度数単位や、1時間未満の時間の単位が六十進法であるのは、これに由来する。

六十進法 - Wikipedia

⇧ 今現在の日時の定義は、バビロニアの「六十進法」が起源ってことになるっぽいですな。

ただ、過去には「六十進法」という概念も無かった頃が当然あったわけで(現在でも未開の文明で活動する人類がいるかもしれず、日時の概念が無い生活をしているケースがあるかもしれませんが)、

原初の区分

時刻表現として物理的に覚知可能な最小単位は、日の出日の入によるで、より小さな分割は人為的で分割方法により様々な時法がある。極圏白夜など日の出も日の入りもない時期や悪天候の際は、太陽の運行を判別できず、日や時刻の特定が困難である。

時刻 - Wikipedia

⇧この「日の出」と「日の入」の状態(明るくなったら活動を開始して、暗くなったら活動を終了する)から、「六十進法」を基準に時間の単位を決めるに至ったのは、何なのか分からないですが、太陽の動きが「十二進法」「六十進法」で説明がついてしまったが故に、今現在の1日24時間って定義が生まれたってことなんですかね。

では、1年が12ヵ月であることや、1ヵ月の日数はどのようにして決まったのか?

(つき、がつ、げつ、month)は、時間単位の一つ。の中間にある単位で、一を12分したであ。現在世界で標準的に用いられるグレゴリオ暦は、修正元のユリウス暦の月を汲み、1か月の日数は30もしくは31日を基本とし、2月のみ通常(平年)は28日、400年間に97回ある閏年には29日としている

月 (暦) - Wikipedia

概念

時間単位の「月」は、日次経過を知る際に天体相(満ち欠け)の様子を見ることで容易に認識できることから生じた。新月から次の新月までの周期を指す1朔望月が約29.530 589であることから30日(大の月)もしくは29日(小の月)を1か月としていた

月 (暦) - Wikipedia

この周期単位を用いる太陰暦では、1は約354.4日となってしまい、季節の循環を司る太陽天球を一巡する周期である365.2422日と比べて、3年で1か月程度ずれが積み重なる。このため、閏月を適宜加える太陰太陽暦が作られた。しかし、どのように閏月を設定すべきかという置閏法の問題が残った

月 (暦) - Wikipedia

一年を太陽の運行から定める太陽暦は、ナイル川氾濫太陽年の周期で起こる古代エジプトで発明され、古代ローマユリウス暦に取り込まれてヨーロッパに広まり、改暦を経たグレゴリオ暦として世界中に広まり、時間の「月」はその基準を天体の月から太陽へ移されることになった

月 (暦) - Wikipedia

⇧ 月の周期を元にしていると。

とりあえず、今現在の日時は、上記のような考えに基づいていると。

ザックリまとめると、

  1. 1日は24時間
    • 1時間は60分
    • 1分は60秒
  2. 1ヵ月はおよそ30日
  3. 1年はおよそ365日、故に1年は12ヵ月

⇧ というような感じで、落ち着いたと。(ミリ秒、ナノ秒、マイクロ秒とか細かい部分は割愛)

で、「閏年」が関係してくるのが、2、3になってくると。

今回「うるう秒」は対象外ということで。

閏年とは

Wikipediaさんによりますと、

閏年(うるうどし、じゅんねん、英語leap yearintercalary year)とは、のあるである。これに対し、閏年ではない年を平年(へいねん、英語common year)と呼ぶ。

閏年 - Wikipedia

⇧ とあり、「閏(うるう)」はと言うと、

(うるう、じゅん)は、において1年の月数や日数が普段の平年)よりも多いこと、または1日の秒数が普段の日よりも多いことをいう。またはその余分なのこと。なお、「閏」の字が常用漢字表に含まれていないため、うるう年うるう月うるう日うるう秒と書かれる場合もある。

閏 - Wikipedia

解説

季節とのずれを調節するために入れられる。“うるう”という読みは、閏と潤を混同して“うるおう”という読みがなまったものだという。なお暦学理論上、閏週という考えもあり得るが、今日の暦法において採用しているものはない。

閏 - Wikipedia

閏月・閏日

多くの太陰太陽暦においては約3年に1度、余分な1か月(閏月)を入れる。

多くの太陽暦においては約4年に1度、余分な1日(閏日)を入れる。

閏 - Wikipedia

閏月または閏日が入れられる年のことを閏年という。

閏 - Wikipedia

⇧ ということらしい。

世間一般的には、少なくとも日本では、4年に一度の頻度で「閏年」になるという認識っぽいですな。

閏日(うるうび、じゅんじつ)とは、太陽暦では季節天動説では太陽の運行)とのずれとを、太陰暦では朔望月の運行)とのずれを補正する暦日のことである。

閏日 - Wikipedia

西暦(ユリウス暦グレゴリオ暦

西暦は代表的な太陽暦である。

ユリウス暦では4年に1度、グレゴリオ暦では400年に97度、閏年とし、2月の日数を1日増加させる。このとき付け加えられた日が閏日である。グレゴリオ暦では加えられる日は2月29日である。ユリウス暦からグレゴリオ暦に改暦した歴史的な理由から2月24日閏日とする国もある。

閏日 - Wikipedia

ヒジュラ暦イスラム暦

ヒジュラ暦は、代表的な太陰暦(純粋太陰暦)であり、ズー・アル=ヒッジャ月(第12月)の30日目を閏日とする

閏日 - Wikipedia

⇧ 国によって変わってくるんかな?

2月24日を閏日とする国もあるってことなのですが、そうなると毎年、2月24日があるので、おかしなことになりそうなんだけど...

Javaとデータベースの関係を整理しておく

で、Webアプリケーションとかだと、データ型の観点として、大きく分けると、

  1. プログラミング言語
  2. データベース

の2つが登場すると思うんですよね。

で、プログラミング言語としてJavaを利用している場合、

jcp.org

⇧ データベースとのデータ型の調整は、JDBCJava Database Connectivity)が良しなに行ってくれるんだと思われる、多分。

実際には、

⇧それぞれのベンダー毎が提供してくれているデータベース向けのJDBC driverのjarファイルとかを利用する感じになると。

JDBC Driversとかを提供しているベンダーの一覧みたいな情報は無さそうなので、

Maven Repositoryとかで、頑張って確認する感じになるんかな(Maven Repositoryで公開せず、独自のRepositoryで管理しているベンダーもあるので、利用するデータベース次第ではMaven Repository以外を探す必要もありそう)

で、JDBCの仕様を見た感じ、

⇧ データベースに登録できる値としては、Java 8で登場したjava.timeパッケージに属する日時系のクラスのデータ型にも対応している。

ところが、JDBCのデフォルトの機能でデータベースのレコードを取得してきた場合(ResultSetにデータベースから取得してきたレコードなどが格納される)、

⇧どうやら、java.timeパッケージに対応して無さそうなのよね...

と言うか、JDBCの仕様が探し辛過ぎるんだが...

バージョンもどれが最新なのかが分かり辛いし...

ボヤいてしまいましたが、このあたりは、各ベンダーのJDBC Driverの実装に委ねられるようで(JDBCの仕様に記載が無いのはどうかと思うが...)

ちなみに、

stackoverflow.com

PostgreSQLJDBC Driverは対応しているらしい。

ソースコードを確認した感じでは、

github.com

■抜粋 https://github.com/pgjdbc/pgjdbc/blob/master/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java

/*
 * Copyright (c) 2004, PostgreSQL Global Development Group
 * See the LICENSE file in the project root for more information.
 */

package org.postgresql.jdbc;

import static org.postgresql.util.internal.Nullness.castNonNull;

import org.postgresql.Driver;
import org.postgresql.PGRefCursorResultSet;
import org.postgresql.PGResultSetMetaData;
import org.postgresql.core.BaseConnection;
import org.postgresql.core.BaseStatement;
import org.postgresql.core.Encoding;
import org.postgresql.core.Field;
import org.postgresql.core.Oid;
import org.postgresql.core.Provider;
import org.postgresql.core.Query;
import org.postgresql.core.ResultCursor;
import org.postgresql.core.ResultHandlerBase;
import org.postgresql.core.TransactionState;
import org.postgresql.core.Tuple;
import org.postgresql.core.TypeInfo;
import org.postgresql.core.Utils;
import org.postgresql.util.ByteConverter;
import org.postgresql.util.GT;
import org.postgresql.util.HStoreConverter;
import org.postgresql.util.JdbcBlackHole;
import org.postgresql.util.NumberParser;
import org.postgresql.util.PGbytea;
import org.postgresql.util.PGobject;
import org.postgresql.util.PGtokenizer;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;

import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.index.qual.Positive;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.qual.PolyNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
import org.checkerframework.dataflow.qual.Pure;

import java.io.ByteArrayInputStream;
import java.io.CharArrayReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Date;
import java.sql.NClob;
import java.sql.PreparedStatement;
import java.sql.Ref;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.RowId;
import java.sql.SQLException;
import java.sql.SQLType;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

public class PgResultSet implements ResultSet, PGRefCursorResultSet {

  // needed for updateable result set support

...省略

  private @Nullable OffsetDateTime getOffsetDateTime(int i) throws SQLException {
    byte[] value = getRawValue(i);
    if (value == null) {
      return null;
    }

    int col = i - 1;
    int oid = fields[col].getOID();

    // TODO: Disallow getting OffsetDateTime from a non-TZ field
    if (isBinary(i)) {
      if (oid == Oid.TIMESTAMPTZ || oid == Oid.TIMESTAMP) {
        return getTimestampUtils().toOffsetDateTimeBin(value);
      } else if (oid == Oid.TIMETZ) {
        // JDBC spec says timetz must be supported
        return getTimestampUtils().toOffsetTimeBin(value).atDate(LOCAL_DATE_EPOCH);
      }
    } else {
      // string

      if (oid == Oid.TIMESTAMPTZ || oid == Oid.TIMESTAMP )  {

        OffsetDateTime offsetDateTime = getTimestampUtils().toOffsetDateTime(castNonNull(getString(i)));
        if ( offsetDateTime != OffsetDateTime.MAX && offsetDateTime != OffsetDateTime.MIN ) {
          return offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC);
        } else {
          return offsetDateTime;
        }

      }
      if ( oid == Oid.TIMETZ ) {
        return getTimestampUtils().toOffsetDateTime(castNonNull(getString(i)));
      }
    }

    throw new PSQLException(
        GT.tr("Cannot convert the column of type {0} to requested type {1}.",
            Oid.toString(oid), "java.time.OffsetDateTime"),
        PSQLState.DATA_TYPE_MISMATCH);
  }
  
...省略

  @Override
  public <T> @Nullable T getObject(@Positive int columnIndex, Class<T> type) throws SQLException {
    if (type == null) {
      throw new SQLException("type is null");
    }
    int sqlType = getSQLType(columnIndex);
    if (type == BigDecimal.class) {
      if (sqlType == Types.NUMERIC || sqlType == Types.DECIMAL) {
        return type.cast(getBigDecimal(columnIndex));
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == String.class) {
      if (sqlType == Types.CHAR || sqlType == Types.VARCHAR) {
        return type.cast(getString(columnIndex));
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == Boolean.class) {
      if (sqlType == Types.BOOLEAN || sqlType == Types.BIT) {
        boolean booleanValue = getBoolean(columnIndex);
        if (wasNull()) {
          return null;
        }
        return type.cast(booleanValue);
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == Short.class) {
      if (sqlType == Types.SMALLINT) {
        short shortValue = getShort(columnIndex);
        if (wasNull()) {
          return null;
        }
        return type.cast(shortValue);
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == Integer.class) {
      if (sqlType == Types.INTEGER || sqlType == Types.SMALLINT) {
        int intValue = getInt(columnIndex);
        if (wasNull()) {
          return null;
        }
        return type.cast(intValue);
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == Long.class) {
      if (sqlType == Types.BIGINT) {
        long longValue = getLong(columnIndex);
        if (wasNull()) {
          return null;
        }
        return type.cast(longValue);
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == BigInteger.class) {
      if (sqlType == Types.BIGINT) {
        long longValue = getLong(columnIndex);
        if (wasNull()) {
          return null;
        }
        return type.cast(BigInteger.valueOf(longValue));
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == Float.class) {
      if (sqlType == Types.REAL) {
        float floatValue = getFloat(columnIndex);
        if (wasNull()) {
          return null;
        }
        return type.cast(floatValue);
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == Double.class) {
      if (sqlType == Types.FLOAT || sqlType == Types.DOUBLE) {
        double doubleValue = getDouble(columnIndex);
        if (wasNull()) {
          return null;
        }
        return type.cast(doubleValue);
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == Date.class) {
      if (sqlType == Types.DATE) {
        return type.cast(getDate(columnIndex));
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == Time.class) {
      if (sqlType == Types.TIME) {
        return type.cast(getTime(columnIndex));
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == Timestamp.class) {
      if (sqlType == Types.TIMESTAMP
              || sqlType == Types.TIMESTAMP_WITH_TIMEZONE
      ) {
        return type.cast(getTimestamp(columnIndex));
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == Calendar.class) {
      if (sqlType == Types.TIMESTAMP
              || sqlType == Types.TIMESTAMP_WITH_TIMEZONE
      ) {
        Timestamp timestampValue = getTimestamp(columnIndex);
        if (timestampValue == null) {
          return null;
        }
        Calendar calendar = Calendar.getInstance(getDefaultCalendar().getTimeZone());
        calendar.setTimeInMillis(timestampValue.getTime());
        return type.cast(calendar);
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == Blob.class) {
      if (sqlType == Types.BLOB || sqlType == Types.BINARY || sqlType == Types.BIGINT) {
        return type.cast(getBlob(columnIndex));
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == Clob.class) {
      if (sqlType == Types.CLOB || sqlType == Types.BIGINT) {
        return type.cast(getClob(columnIndex));
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == java.util.Date.class) {
      if (sqlType == Types.TIMESTAMP) {
        Timestamp timestamp = getTimestamp(columnIndex);
        if (timestamp == null) {
          return null;
        }
        return type.cast(new java.util.Date(timestamp.getTime()));
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == Array.class) {
      if (sqlType == Types.ARRAY) {
        return type.cast(getArray(columnIndex));
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == SQLXML.class) {
      if (sqlType == Types.SQLXML) {
        return type.cast(getSQLXML(columnIndex));
      } else {
        throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
                PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else if (type == UUID.class) {
      return type.cast(getObject(columnIndex));
    } else if (type == InetAddress.class) {
      String inetText = getString(columnIndex);
      if (inetText == null) {
        return null;
      }
      int slash = inetText.indexOf("/");
      try {
        return type.cast(InetAddress.getByName(slash < 0 ? inetText : inetText.substring(0, slash)));
      } catch (UnknownHostException ex) {
        throw new PSQLException(GT.tr("Invalid Inet data."), PSQLState.INVALID_PARAMETER_VALUE, ex);
      }
      // JSR-310 support
    } else if (type == LocalDate.class) {
      return type.cast(getLocalDate(columnIndex));
    } else if (type == LocalTime.class) {
      return type.cast(getLocalTime(columnIndex));
    } else if (type == LocalDateTime.class) {
      return type.cast(getLocalDateTime(columnIndex));
    } else if (type == OffsetDateTime.class) {
      return type.cast(getOffsetDateTime(columnIndex));
    } else if (type == OffsetTime.class) {
      return type.cast(getOffsetTime(columnIndex));
    } else if (PGobject.class.isAssignableFrom(type)) {
      Object object;
      if (isBinary(columnIndex)) {
        byte[] byteValue = castNonNull(thisRow, "thisRow").get(columnIndex - 1);
        object = connection.getObject(getPGType(columnIndex), null, byteValue);
      } else {
        object = connection.getObject(getPGType(columnIndex), getString(columnIndex), null);
      }
      return type.cast(object);
    }
    throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
            PSQLState.INVALID_PARAMETER_VALUE);
  }

...省略


  protected PgResultSet upperCaseFieldLabels() {
    for (Field field: fields ) {
      field.upperCaseLabel();
    }
    return this;
  }
}
   

⇧ とりあえず、PostgreSQLについては、getObjectの戻り値として、java.timeパッケージの日時系のクラスの型を戻り値としているので、Object型の変数の中にjava.timeパッケージの日時系のクラスの型の値が格納される仕組みになっていることは確認できましたと。

所謂、

ポリモーフィズムpolymorphism)とは、それぞれ異なる型に一元アクセスできる共通接点の提供、またはそれぞれ異なる型の多重定義を一括表現できる共通記号の提供を目的にした、型理論またはプログラミング言語理論英語版の概念および実装である。この用語は、有機組織および生物の種は様々な形態と段階を持つという生物学の概念からの借用語である多態性多相性と邦訳されることが多い。

ポリモーフィズム - Wikipedia

ポリモーフィズムは、通常以下の三種に分けられる。

アドホック多相
ad hoc polymorphism)
恣意的な型の集合に一つの共通接点を提供する。関数オーバーロードMix-inのいち実装、型クラスなど。
パラメトリック多相
(parametric polymorphism)
詳細化されていない型要素を内包する抽象的な型に記号表現を提供する。ジェネリクス関数型言語の型構築子など。
サブタイピング
(subtyping)
サブタイプ多相(subtype polymorphism)やインクルージョン多相(inclusion polymorphism)とも。上位型をその下位型の数々で代替できるようにするオブジェクト指向多態性はこれを指す。

ポリモーフィズム - Wikipedia

⇧「ポリモーフィズム多態性)」の「サブタイピング」に該当する仕組みを利用しているということになるんかね、「オブジェクト指向」ってやつですかね。

念のため、getObjectの戻り値について、java.timeパッケージの日時系のクラスの型を期待しているカラムの値である場合は、戻り値をキャストしてjava.timeパッケージの日時系のクラスの型の値として取り出せるかの動作確認は必要ですかね。

他のベンダーのデータベースのJDBC Driverが対応しているのかは不明ですな...

Javaの標準API閏年の扱いとかどうなっているのか

で、JDBCの仕様としては、データベースに登録するデータとしては、Javajava.timeパッケージの日時系のクラスの型にも対応してくれているっぽいのが確認できたので、Javaを利用しているアプリケーションの場合は、

  1. プログラミング言語

の方、つまり、Javaの処理をコーディングする観点のみに考慮事項を絞り込めるってことになるかと。

Wikipediaの説明にもありましたが、

nagise.hatenablog.jp

⇧ 上記サイト様でも「閏年」の定義がまちまちな点を上げておられますと。

なので、まずは開発しているシステムで「閏年」をどう定義するかを、開発に関わる全てのメンバーに共有しておく必要がありそう。

で、Java 8から導入されているjava.timeパッケージに属するクラスの1つである、「java.time.Year」のAPIドキュメントの説明を見ると、

docs.oracle.com

ISO暦における年は、現代のグレゴリオ/ユリウス暦体系における年にしか適合しないことに注意してください。ロシアの一部では、1920年まで現代のグレゴリオ/ISOのルールに切換えていませんでした。このため、歴史的な年の取り扱いには注意が必要です。

https://docs.oracle.com/javase/jp/8/docs/api/java/time/Year.html

このクラスで表される年はISO-8601標準に準拠し、先発番号付け方式を採用しています。1年の前に0年があり、0年の前に-1年があります。

https://docs.oracle.com/javase/jp/8/docs/api/java/time/Year.html

ISO-8601暦体系は、世界中の大部分で現在使われている近代の常用暦体系です。これは、現在のうるう年のルールがすべての時間に適用される、先発グレゴリオ暦体系と同等です。今日作成されているほとんどのアプリケーションには、ISO-8601のルールが完全に適しています。ただし、歴史的な日付を使用し、それらが正確であることを必要とするアプリケーションには、ISO-8601の方法が適さないことがわかります。

https://docs.oracle.com/javase/jp/8/docs/api/java/time/Year.html

これは値ベースのクラスです。Yearインスタンスに対して、アイデンティティの影響を受けやすい操作(参照型等価演算子(==)、アイデンティティ・ハッシュ・コード、同期化など)を使用すると、予測できない結果が生じることがあるため、使わないようにしてください。比較する場合は、equalsメソッドを使用することをお薦めします。

https://docs.oracle.com/javase/jp/8/docs/api/java/time/Year.html

⇧ とあるので、「ISO-8601標準」における「閏年」の定義に則した処理としてコーディングしておく必要がありますと。

ISO-8601標準」はと言うと、

ISO 8601は、日付時刻の表記に関するISOの国際規格である。この規格の主眼は、日付時刻の記述順序が国や文化によってまちまちであるものを、大→小の順序(ビッグエンディアン big-endian)を貫徹して、日付・時刻の記述順序をただ一種類に標準化していることにある。

ISO 8601 - Wikipedia

年月日の区切り記号は「-」(ハイフン)のみを用い、「/」などを禁じている。また時刻表現を24時制だけに限定している。

  • 2022年9月4日を、2022-09-04(拡張形式)もしくは20220904(基本形式)と表記する
  • 2022年9月4日の時刻として 16時07分48.53秒 を併せて表記する場合は、2022-09-04T16:07:48.53(拡張形式)または20220904T160748.53(基本形式)と表記する。すなわち記号 T で区切った後に時刻を続ける。

上記以外に、日の番号暦週の番号タイムゾーン継続時間期間などの記述方法についても規定している。

ISO 8601 - Wikipedia

⇧とのこと。

で、気になるのが、

年の表記(0000年~9999年)

日付の表記にはグレゴリオ暦を用いる。これはグレゴリオ暦が導入された1582年10月15日以前にも適用される(「先発グレゴリオ暦」も参照)。ただし、0000年から1582年の範囲は、事前に通信の送信側と受信側との間での合意がある場合にだけ使うことができる一般(たとえばJavaライブラリ)には1582年以前の日付表現はユリウス暦と解釈されるが、ISO 8601 にはそのような措置はない。そのため、それらの日付表現をこのISO準拠にするにはグレゴリオ暦への換算が必要である。

ISO 8601 - Wikipedia

⇧ って部分が、java.timeパッケージにも当てはまるのか。

 端的に言えばこのISO 8601は先発グレゴリオ暦で、1582年10月15日以前にも適用される。ただし、0000年から1582年の範囲は、事前に通信の送信側と受信側との間での合意がある場合にのみ使うことができるとされている。

 このISO 8601に相当するのが IsoChronology である。Java8 以降の Date and Time API での標準はこのIsoChronologyとなっている。

西暦1年は閏年か? - プログラマーの脳みそ

⇧ 上記サイト様によりますと、「IsoChronology」を見れば分かるってことなんかな?

docs.oracle.com

この暦はISO暦体系のルールを定義します。この暦体系は、ISO-8601規格に基づいており、事実上の世界暦です。

https://docs.oracle.com/javase/jp/8/docs/api/java/time/chrono/IsoChronology.html

このメソッドは、時系列全体にわたって、うるう年の現在のルールを適用します。一般に、年は4で割り切れる場合にうるう年です。ただし、400で割り切れる年を除き、100で割り切れる年はうるう年ではありません。

https://docs.oracle.com/javase/jp/8/docs/api/java/time/chrono/IsoChronology.html#isLeapYear-long-

たとえば、1904年は4で割り切れるうるう年です。1900年は100で割り切れるため、うるう年ではありませんでしたが、2000年は400で割り切れるため、うるう年でした。

https://docs.oracle.com/javase/jp/8/docs/api/java/time/chrono/IsoChronology.html#isLeapYear-long-

計算は先発暦で、遠い将来および遠い過去にも同じルールが適用されます。これは歴史的には正確ではありませんが、ISO-8601規格には正確です。

https://docs.oracle.com/javase/jp/8/docs/api/java/time/chrono/IsoChronology.html#isLeapYear-long-

⇧う~む、

一般(たとえばJavaライブラリ)には1582年以前の日付表現はユリウス暦と解釈されるが、ISO 8601 にはそのような措置はない。そのため、それらの日付表現をこのISO準拠にするにはグレゴリオ暦への換算が必要である。

について、java.time.IsoChronology.isLeapYearの説明を見る限り、解消されているような気がする。

とりあえず、java.timeパッケージを利用しておけば、

  • 開発しているアプリケーションがISO-8601に準拠している

と決めているのであれば、Javaの標準APIが「閏年」に対応してくれるってことですかね。

流石に、Java 8以前のバージョンを使っているシステムは無いと信じたいので、Java 8以降を利用しているシステムであり、且つ、java.timeパッケージで日時系の処理をコーディングしているのであれば、「閏年」問題を回避できそうということですかね。

フロントエンド側は別途対応が必要だと思いますが、サーバーサイド側をJavaで開発している場合に、java.timeパッケージを利用して日時処理をするようにしておけば、サーバーサイド側での懸念は解消されるということですかね。

閏年」厄介ですな...

閏年」の問題が繰り返されているところを見るに、アンチパターンやベストプラクティス的なノウハウは公開されても良い気はするんですけどね...

ノウハウを共有して、人口に膾炙するようにしておけば、別の課題に工数をかけることができて、その課題が解決できた時のノウハウを共有して、と言った感じでノウハウの共有を蓄積していくサイクルができれば、IT業界全体的な生産性が上がりそうな気はするんですが、まぁ、冒頭にもあった通り、IT業界は閉鎖的な感じなので(『日本IT国家戦略を技術面・人材面から支える』と謳っているIPAからしてやる気が無さそうなので)、ノウハウが共有されることは期待できそうにないということですかね...

まぁ、長文になってしまったのですが、1つ改めて分かったことは、プログラミングにおいて日時の扱いは難しいってことですな...

毎度モヤモヤ感が半端ない...

今回はこのへんで。