001/*
002 * Copyright (c) 2009 The openGion Project.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
013 * either express or implied. See the License for the specific language
014 * governing permissions and limitations under the License.
015 */
016package org.opengion.fukurou.util;
017
018import java.util.Calendar;
019import java.util.Locale;
020import java.util.Map;
021import java.util.HashMap;
022import java.text.DateFormat;
023import java.text.SimpleDateFormat;
024import java.text.ParseException;
025import java.util.List;                                                                                                  // 7.0.5.0 (2019/09/09)
026import java.util.ArrayList;                                                                                             // 7.0.5.0 (2019/09/09)
027
028import org.opengion.fukurou.system.DateSet;                                                             // 6.4.2.0 (2016/01/29)
029import org.opengion.fukurou.system.OgRuntimeException ;                                 // 6.4.2.0 (2016/01/29)
030
031import static org.opengion.fukurou.system.HybsConst.BUFFER_SMALL;               // 6.1.0.0 (2014/12/26) refactoring
032
033/**
034 * HybsDateUtil.java は、共通的に使用される Date,Calender関連メソッドを集約した、staticメソッドのみで構成されるクラスです。
035 *
036 * @og.rev 5.5.7.2 (2012/10/09) 新規作成
037 *
038 * @og.group ユーティリティ
039 *
040 * @version  5.5
041 * @author       Kazuhiko Hasegawa
042 * @since    JDK7.0,
043 */
044public final class HybsDateUtil {
045
046        /** 各種フォーマットを簡易的に表した文字列 */
047        /** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
048        private static final Map<String,String> DATE_FORMAT = new HashMap<>();
049        static {
050                DATE_FORMAT.put( "Y4"           ,"yyyy"                                 );      // 6.9.2.1 (2018/03/12)
051                DATE_FORMAT.put( "YMD"          ,"yyyyMMdd"                             );
052                DATE_FORMAT.put( "Y2MD"         ,"yyMMdd"                               );
053                DATE_FORMAT.put( "YM"           ,"yyyyMM"                               );
054                DATE_FORMAT.put( "MD"           ,"MMdd"                                 );      // 5.5.5.2 (2012/08/18)
055                DATE_FORMAT.put( "HMS"          ,"HHmmss"                               );
056                DATE_FORMAT.put( "HM"           ,"HHmm"                                 );      // 6.7.4.1 (2017/02/17)
057                DATE_FORMAT.put( "YMDHMS"       ,"yyyyMMddHHmmss"               );
058                DATE_FORMAT.put( "YMDHM"        ,"yyyyMMddHHmm"                 );      // 7.0.5.0 (2019/09/16)
059                DATE_FORMAT.put( "YMDH"         ,"yyyyMMddHH"                   );      // 7.0.5.0 (2019/09/16)
060                DATE_FORMAT.put( "EEE"          ,"EEE"                                  );
061                DATE_FORMAT.put( "YMDF"         ,"yyyy/MM/dd"                   );
062                DATE_FORMAT.put( "YMD-F"        ,"yyyy-MM-dd"                   );      // 8.0.1.2 (2021/11/19) HTML5 type="date"
063                DATE_FORMAT.put( "Y2MDF"        ,"yy/MM/dd"                             );
064                DATE_FORMAT.put( "YMF"          ,"yyyy/MM"                              );
065                DATE_FORMAT.put( "YM-F"         ,"yyyy-MM"                              );      // 8.0.1.2 (2021/11/19) HTML5 type="month"
066                DATE_FORMAT.put( "HMSF"         ,"HH:mm:ss"                             );
067                DATE_FORMAT.put( "HMF"          ,"HH:mm"                                );      // 6.7.4.1 (2017/02/17)
068                DATE_FORMAT.put( "YMDHMSF"      ,"yyyy/MM/dd HH:mm:ss"  );
069                DATE_FORMAT.put( "YMDHMF"       ,"yyyy/MM/dd HH:mm"             );      // 7.0.5.0 (2019/09/16)
070                DATE_FORMAT.put( "YMDHM-F"      ,"yyyy-MM-dd'T'HH:mm"   );      // 8.0.1.2 (2021/11/19) HTML5 type="datetime-local"
071                DATE_FORMAT.put( "MDF"          ,"MM/dd"                                );      // 5.5.0.2 (2012/03/09)
072                DATE_FORMAT.put( "MDEF"         ,"MM/dd(EEE)"                   );      // 5.5.0.2 (2012/03/09) 曜日
073                DATE_FORMAT.put( "MDHMF"        ,"MM/dd HH:mm"                  );      // 7.0.0.1 (2018/10/09)
074                DATE_FORMAT.put( "MD2F"         ,"MM月dd日"                               );      // 5.5.5.2 (2012/08/18) 漢字
075                DATE_FORMAT.put( "HM2F"         ,"HH時mm分"                               );      // 7.0.0.1 (2018/10/09) 漢字
076                DATE_FORMAT.put( "MDHM2F"       ,"MM月dd日 HH時mm分"        );      // 7.0.0.1 (2018/10/09) 漢字
077                DATE_FORMAT.put( "GYMDF"        ,"GGGGyyyy年MM月dd日"      );      // 5.5.0.2 (2012/03/09) 和暦
078                DATE_FORMAT.put( "G2YMDF"       ,"Gyyyy/MM/dd"                  );      // 5.5.0.2 (2012/03/09) 和暦
079                DATE_FORMAT.put( "GYMF"         ,"GGGGyyyy年MM月"         );      // 5.5.0.2 (2012/03/09) 和暦
080                DATE_FORMAT.put( "GYF"          ,"GGGGyyyy"                             );      // 5.5.0.2 (2012/03/09) 和暦
081        }
082
083        private static final int DD = 1000 * 60 * 60 * 24 ;             // ミリ秒 → 日
084        private static final int HH = 1000 * 60 * 60 ;                  // ミリ秒 → 時
085        private static final int MM = 1000 * 60 ;                               // ミリ秒 → 分
086        private static final int SS = 1000 ;                                    // ミリ秒 → 秒
087
088        // ※ Locale.of は、JDK19 で採用なので、JDK17との互換性がない。
089//      private static final Locale JA_JP_JP = Locale.of( "ja", "JP", "JP" );           // 8.5.3.2 (2023/10/13) JDK21対応
090        @SuppressWarnings( "deprecation" )                                                                                      // 8.5.3.2 (2023/10/13) JDK21対応
091        private static final Locale JA_JP_JP = new Locale("ja","JP","JP");                      // 旧方式で、警告が出る。
092
093        /**
094         * デフォルトコンストラクターをprivateにして、
095         * オブジェクトの生成をさせないようにする。
096         *
097         * @og.rev 5.5.7.2 (2012/10/09) 新規作成
098         */
099        private HybsDateUtil() {}
100
101        /**
102         * 指定の文字列から、以下の文字を削除した文字列を返します。
103         * '/' , '-' , ' ' , ':' の数字以外の文字を含むフォーマットされた
104         * 日付文字列を、日付データだけに変換する場合に利用することを想定しています。
105         * よって、マイナス記号や、小数点、コンマなども削除されます。
106         * このメソッドでは、日付としての整合性や桁チェックは行いませんが、
107         * 桁数は、6桁、8桁、14桁のどれかに、合わせます。
108         * 「yyyy/MM/dd HH:mm:ss」 形式を基準としますが、「yyyy/M」「yyyy/M/d」「yy/M/d」「M/d」
109         * 「HH:mm:ss」「H:m」形式にも、対応します。
110         * "/" が、"-" に変更されているケースも対応可能ですが、月/年 形式や、英語、日本語の
111         * 月表示には未対応です。
112         *
113         * 引数が、null の場合は、ゼロ文字列に、変換します。
114         *
115         * ※ 6.0.2.5 (2014/10/31) 桁数チェックだけは行います。
116         *   桁数は、6桁、8桁、14桁のどれかに、合わせます。
117         *
118         * @og.rev 5.5.7.2 (2012/10/09) 新規作成
119         * @og.rev 5.5.8.3 (2012/11/17) 数字のみ返す仕様だったが、対象以外の文字入力はそのまま返すよう変更
120         * @og.rev 6.0.2.5 (2014/10/31) 簡易的な桁数チェックだけは行います。
121         * @og.rev 6.2.3.0 (2015/05/01) 内部処理を大幅に変更します。
122         * @og.rev 8.0.1.2 (2021/11/19) yyyy-MM-ddTHH:mm 対応(スペースではなくTで区切る)
123         * @og.rev 8.5.5.1 (2024/02/29) switch文にアロー構文を使用
124         *
125         * @param       value 任意の文字列(例:2001/04/17 15:48:22)
126         *
127         * @return      数字だけで構成される文字列(例:20010417154822)(nullはゼロ文字列を返します)
128         * @og.rtnNotNull
129         */
130        public static String parseNumber( final String value ) {
131                if( value == null || value.isEmpty() ) { return ""; }
132
133                // 年 や、年月日 が省略された場合は、実行日をセットする。
134                final String today = DateSet.getDate( "yyyyMMdd" );
135
136                String val = value.trim();
137                val = val.replaceAll( "-" , "/" );                      // yyyy-MM-dd 形式を、yyyy/MM/dd 形式にする。
138
139                // 8.0.1.2 (2021/11/19) yyyy-MM-ddTHH:mm 対応(スペースではなくTで区切る)
140                val = val.replaceAll( "T" , " " );
141
142                final int ad = val.indexOf( ' ' ) ;
143                String ymd = val  ;
144                String hms = null ;
145
146                if( ad > 0 ) {                                          // スペースがあれば、年月日 と 時分秒 に別れる。
147                        ymd = val.substring( 0,ad );
148                        hms = val.substring( ad+1 );
149                }
150                else if( val.indexOf( ':' ) > 0 ) {
151                        ymd = today;                                    // 年月日 は今日になる。
152                        hms = val;
153                }
154
155                final StringBuilder buf = new StringBuilder( BUFFER_SMALL );
156
157                if( ymd != null ) {
158                        final String[] ymdSp = ymd.split( "/" );
159                        // 8.5.5.1 (2024/02/29) switch文にアロー構文を使用
160//                      switch( ymdSp.length ) {
161//                              case 1 : buf.append( ymdSp[0] );        break;                                  // "/" が存在しない。
162//                              case 2 : if( ymdSp[0].length() < 4 ) {                                          // MM/dd のケース。yy/MM は想定外
163//                                                      buf.append( today.substring( 0,4 ) );                   // yyyy の年を設定
164//                                               }
165//                                               buf.append( addZero( ymdSp[0] ) ).append( addZero( ymdSp[1] ) );       break;
166//                              default:  if( ymdSp[0].length() == 2 ) {                                        // yy/MM/dd のケースを想定
167//                                                      buf.append( today.substring( 0,2 ) );                   // yy の年の先頭2桁を設定
168//                                               }
169//                                               buf.append( ymdSp[0] )
170//                                                      .append( addZero( ymdSp[1] ) )
171//                                                      .append( addZero( ymdSp[2] ) ); break;
172                        switch( ymdSp.length ) {
173                                case 1 -> buf.append( ymdSp[0] );                                               // "/" が存在しない。
174                                case 2 -> {
175                                                if( ymdSp[0].length() < 4 ) {                                           // MM/dd のケース。yy/MM は想定外
176                                                        buf.append( today.substring( 0,4 ) );                   // yyyy の年を設定
177                                                }
178                                                buf.append( addZero( ymdSp[0] ) ).append( addZero( ymdSp[1] ) );
179                                }
180                                default -> {
181                                                 if( ymdSp[0].length() == 2 ) {                                 // yy/MM/dd のケースを想定
182                                                        buf.append( today.substring( 0,2 ) );                   // yy の年の先頭2桁を設定
183                                                }
184                                                buf.append( ymdSp[0] )
185                                                        .append( addZero( ymdSp[1] ) )
186                                                        .append( addZero( ymdSp[2] ) );
187                                }
188                        }
189                }
190                if( hms != null ) {
191                        final String[] hmsSp = hms.split( ":" );                                                // HH:mm:ss
192                        // 8.5.5.1 (2024/02/29) switch文にアロー構文を使用
193//                      switch( hmsSp.length ) {
194//                              case 1 : buf.append( hmsSp[0] );                        break;                  // ":" が存在しない。
195//                              case 2 : buf.append( addZero( hmsSp[0] ) )                                      // HH:mm のケース。mm:ss は想定外
196//                                                      .append( addZero( hmsSp[1] ) )
197//                                                      .append( "00" );                                break;
198//                              default: buf.append( addZero( hmsSp[0] ) )                                      // HH:mm:ss のケースを想定
199//                                                      .append( addZero( hmsSp[1] ) )
200//                                                      .append( addZero( hmsSp[2] ) ); break;
201//                      }
202                        switch( hmsSp.length ) {
203                                case 1 -> buf.append( hmsSp[0] );                       // ":" が存在しない。
204                                case 2 -> {
205                                                buf.append( addZero( hmsSp[0] ) )                                       // HH:mm のケース。mm:ss は想定外
206                                                        .append( addZero( hmsSp[1] ) )
207                                                        .append( "00" );
208                                }
209                                default -> {
210                                                buf.append( addZero( hmsSp[0] ) )                                       // HH:mm:ss のケースを想定
211                                                        .append( addZero( hmsSp[1] ) )
212                                                        .append( addZero( hmsSp[2] ) );
213                                }
214                        }
215                }
216
217                return buf.toString();
218        }
219
220        /**
221         * 指定の文字列が、一桁の場合、先頭に 0 を追加します。
222         *
223         * これは、3/4 の様な日付を、0304 にする場合のサポートメソッドです。
224         *
225         * @og.rev 6.2.3.0 (2015/05/01) 新規追加
226         *
227         * @param       val 任意の文字列
228         *
229         * @return      一桁の場合、先頭に 0 を追加
230         */
231        private static String addZero( final String val ) {
232                return val.length() == 1 ? ("0" + val) : val ;
233        }
234
235        /**
236         * 指定の文字列から、yyyy-mm-dd hh:mm:ss 形式の文字列を作成します。
237         *
238         * これは、java.sql.Timestamp オブジェクトを文字列から作成するに当たり、
239         * Timestamp の文字列形式にしなければならないためです。
240         * 桁数は、8桁 または、14桁以外の場合は、変換エラーとします。
241         *
242         * @og.rev 5.5.8.5 (2012/11/27) 新規作成
243         *
244         * @param       value 任意の文字列(例:20010417 or 20010417154822)
245         *
246         * @return      Timestampの文字列形式(例:2001-04-17 00:00:00 or 2001-04-17 15:48:22)
247         */
248        public static String parseTimestamp( final String value ) {
249                if( value == null || value.length() != 8 && value.length() != 14 ) {                            // 6.9.7.0 (2018/05/14) PMD
250                        final String errMsg = "日付文字列は、8桁 または、14桁で指定してください。"
251                                                + " value=[" + value + "]" ;
252                        throw new OgRuntimeException( errMsg );
253                }
254
255                // 6.0.2.5 (2014/10/31) char を append する。
256                final StringBuilder buf = new StringBuilder( BUFFER_SMALL )
257                        .append( value.substring( 0,4 ) ).append( '-' )
258                        .append( value.substring( 4,6 ) ).append( '-' )
259                        .append( value.substring( 6,8 ) ).append( ' ' );
260                if( value.length() == 8 ) {
261                        buf.append( "00:00:00" );
262                }
263                else {
264                        buf.append( value.substring( 8,10  ) ).append( ':' )
265                                .append( value.substring( 10,12 ) ).append( ':' )
266                                .append( value.substring( 12,14 ) );
267                }
268
269                return buf.toString();
270        }
271
272        /**
273         * 日付文字列の桁数の整合性を取ります。
274         * これは、内部で、parseNumber(String) 処理により、不要なフォーマット記号を削除します。
275         * ここでは、基本的には、6文字(yyyyMM)、8文字(yyyyMMdd)、14文字(yyyyMMddHHmmss)
276         * の日付文字列を作成することを想定していますが、指定の桁数以外は、エラーになります。
277         *
278         * 引数が、null         ⇒ 桁数に無関係に、空文字列を返す。
279         * 引数の桁数が一致     ⇒ その値を返す。
280         * 引数の桁数が不一致   ⇒ エラー
281         * ただし、引数の最大長は、14ケタに制限しています。
282         *
283         * このメソッドでは、日付として成立しているかどうか(99999999など)は判定していません。
284         *
285         * @og.rev 5.6.6.0 (2013/07/05) メソッドの内容を移す。
286         *
287         * @param       value   任意の日付け文字列
288         * @param       size    変換したい桁数
289         *
290         * @return      数字だけで構成される文字列(例:20010417154822)(nullはゼロ文字列を返します)
291         * @og.rtnNotNull
292         */
293        public static String parseDate( final String value , final int size ) {
294                return parseDate( value , size , size );                // 最小と最大を同じ値にする。
295        }
296
297        /**
298         * 日付文字列の桁数の整合性を取ります。
299         * これは、内部で、parseNumber(String) 処理により、不要なフォーマット記号を削除します。
300         * ここでは、基本的には、6文字(yyyyMM)、8文字(yyyyMMdd)、14文字(yyyyMMddHHmmss)
301         * の日付文字列を作成することを想定していますが、それ以外の桁数でも下記のルールに従って
302         * 処理されます。
303         *
304         * 引数が、null         ⇒ 桁数に無関係に、空文字列を返す。
305         * 引数の桁数が範囲内   ⇒ 以下の処理を実行する。
306         * 引数の桁数を同じ     ⇒ そのまま返す。
307         * 引数の桁数より大きい ⇒ 余をカットして、引数の最大長にそろえる。
308         * 引数の桁数に足りない ⇒ "20000101000000" の文字列の部分文字列を結合させて、引数の最大長にそろえる。
309         * ただし、引数の最大長は、14ケタに制限しています。
310         *
311         * このメソッドでは、日付として成立しているかどうか(99999999など)は判定していません。
312         *
313         * @og.rev 5.5.7.2 (2012/10/09) 新規作成
314         * @og.rev 5.6.1.1 (2013/02/08) 桁数チェック導入。6桁以下だとエラーにする。
315         * @og.rev 5.6.6.0 (2013/07/05) 桁数チェックの最小-最大指定
316         * @og.rev 6.2.3.0 (2015/05/01) len == maxSize のとき、パース文字列ではなく、元の値を返していた。
317         *
318         * @param       value   任意の日付け文字列
319         * @param       minSize 変換したい桁数の最小値
320         * @param       maxSize 変換したい桁数の最大値
321         *
322         * @return      数字だけで構成される文字列(例:20010417154822)(nullはゼロ文字列を返します)
323         * @og.rtnNotNull
324         */
325        public static String parseDate( final String value , final int minSize , final int maxSize ) {
326                if( value == null ) { return ""; }
327
328                // 引数の最大長は、14ケタに制限しています。
329                if( maxSize > 14 ) {
330                        final String errMsg = "日付登録に許可できる最大桁数は、14ケタです。"
331                                                + " maxSize=[" + maxSize + "]" ;
332                        throw new OgRuntimeException( errMsg );
333                }
334
335                final String rtn = parseNumber( value );
336                final int len = rtn.length() ;
337
338                if( len < minSize || len > maxSize ) {
339                        final String errMsg = "日付文字列は、最小["
340                                                + minSize + "] から、最大[" + maxSize + "]の範囲で指定してください。"
341                                                + " value=[" + value + "]" ;
342                        throw new OgRuntimeException( errMsg );
343                }
344
345                return rtn ;
346        }
347
348        /**
349         * 日付文字列の厳密な整合性チェックを行います。
350         * ここで指定できるのは、8文字(yyyyMMdd)、14文字(yyyyMMddHHmmss)のどちらかの
351         * 数字だけの日付文字列であり、それが、日付として正しいかどうかのチェックを行います。
352         * 正しければ、true を、間違っていれば、false を返します。
353         * ここでは、20120230(2月30日)などの日付や、20120101235960 なども false になります。
354         * 引数が、null および、空文字列の場合も、false を返しますので、避けたい場合は、事前に
355         * 判定しておいてください。
356         *
357         * 内部処理としては、DateFormat で、setLenient( false ) を設定することで、
358         * 日付/時刻解析を厳密に解析するにして、ParseException が発生しないかどうか判定しています。
359         *
360         * @og.rev 5.5.7.2 (2012/10/09) 新規作成
361         *
362         * @param       value  数字だけで構成される日付け文字列
363         *
364         * @return      true:日付として正しい場合/false:日付として間違っている場合
365         */
366        public static boolean isStrict( final String value ) {
367                if( value == null || value.length() != 8 && value.length() != 14 ) { return false; }    // 6.9.7.0 (2018/05/14) PMD Useless parentheses.
368
369                // 日付の厳密なチェック
370                final String form = value.length() == 8 ? "yyyyMMdd" : "yyyyMMddHHmmss" ;
371                final DateFormat formatter = new SimpleDateFormat( form,Locale.JAPAN );
372                formatter.setLenient( false );          // 日付/時刻解析を厳密に行う(false=厳密)
373
374                boolean flag ;
375                try {
376                        formatter.parse( value );
377                        flag = true;
378                }
379                catch( final ParseException ex ) {
380                        flag = false;
381                }
382
383                return flag;
384        }
385
386        /**
387         * 日付関係の情報を簡易的に処理します。
388         *
389         * 引数に与えるのは、{&#064;DATE.XXXX} の XXXX 文字列になります。
390         * この "XXXX" 文字列は、"key prmA prmB prmC" 形式を取ることができます。
391         * 各文字列をスペースで分割して、先頭から変数に割り当てます。
392         * また、prmA の日付文字判定と、数値変換と、prmC の数値変換も行います。
393         * ただし、リクエスト変数の &#064; 文字列変換は、出来ません。
394         *
395         * @og.rev 6.9.2.1 (2018/03/12) 新規追加
396         *
397         * @param   value       日付引数のパラメータ
398         *
399         * @return   メッセージ情報
400         * @og.rtnNotNull
401         * @see         #getDateFormat( String , String ,String , int )
402         */
403        public static String getDateFormat( final String value ) {
404                // {@DATE.XXXX AA BB CC} を分割
405                final String[] vals = StringUtil.csv2Array( value,' ' );                // ダブルクオート内は保持される。
406
407                final String key = vals[0] ;
408
409                String prmA = vals.length >= 2 ? vals[1] : null ;
410                String prmB = vals.length >= 3 ? vals[2] : null ;
411                String prmC = vals.length >= 4 ? vals[vals.length-1] : null ;   // 互換性。最後の値が、CC引数
412
413                // AA 引数のコマンド判定方法(先頭が数字以外)
414                if( StringUtil.isNotNull( prmA ) ) {
415                        final char chA = prmA.charAt(0);
416                        if( chA < '0' || chA > '9' ) {          // 先頭が、数字以外の場合は、コマンドなので、一つずつずらす。
417                                prmC = prmB;
418                                prmB = prmA;
419                                prmA = null;
420                        }
421                }
422
423                // CC 引数を、"H" , "D" , "M" 以外でも使用できるように拡張します。
424                int intC = 0;
425                if( StringUtil.isNotNull( prmC ) ) {
426                        try {
427                                intC = Integer.parseInt( prmC );
428                        }
429                        catch( final NumberFormatException ex ) {
430                                final String errMsg = "CC引数が数字ではありません。value=[" + value + "]"
431                                                                + ex.getMessage() ;
432                                System.err.println( errMsg );
433                        }
434                }
435
436                // prmA が null か、isEmpty() の場合は、現在時刻が使用される。
437                return getDateFormat( key,prmA,prmB,intC );
438        }
439
440        /**
441         * 日付関係の情報を簡易的に処理します。
442         *
443         * 処理コマンドと、CC引数の加減算パラメータを使用しないバージョンです。
444         *
445         * @og.rev 6.9.2.1 (2018/03/12) メソッドの引数を簡素化
446         *
447         * @param   key         フォーマットの予約語
448         * @param   prmA        基準となる日付(nullの場合は、処理時刻)
449         *
450         * @return   メッセージ情報
451         * @og.rtnNotNull
452         * @see         #getDateFormat( String , String ,String , int )
453         */
454        public static String getDateFormat( final String key , final String prmA ) {
455                return getDateFormat( key,prmA,null,0 );
456        }
457
458//      /**
459//       * 日付関係の情報を簡易的に処理します。
460//       *
461//       * CC引数の加減算パラメータは、0 です。
462//       *
463//       * @og.rev 5.7.4.1 (2014/03/14) CC 引数を拡張するため、旧メソッドを再現しておきます。
464//       * @og.rev 6.9.2.1 (2018/03/12) 廃止
465//       *
466//       * @param   key         フォーマットの予約語
467//       * @param   prmA        基準となる日付(nullの場合は、処理時刻)
468//       * @param   prmB        処理コマンド
469//       *
470//       * @return   メッセージ情報
471//       * @og.rtnNotNull
472//       * @see         #getDateFormat( String , String ,String , int )
473//       */
474//      public static String getDateFormat( final String key ,final String prmA ,final String prmB ) {
475//              return getDateFormat( key,prmA,prmB,0 );
476//      }
477
478        /**
479         * 日付関係の情報を簡易的に処理します。
480         *
481         * 第一引数(key) "XXXX" は、日付処理を行うフォーマットの予約語になっています。
482         * ・Y4     :4文字の年データ(yyyy)を扱います。
483         * ・YMD    :8文字の4-2-2年月日データ(yyyyMMdd)を扱います。
484         * ・Y2MD   :6文字の2-2-2年月日データ(yyMMdd)を扱います。
485         * ・YM     :6文字の4-2年月データ(yyyyMM)を扱います。
486         * ・MD     :4文字の月日データ(MMdd)を扱います。
487         * ・HMS    :6文字の2-2-2時分秒データ(HHmmss)を扱います。
488         * ・HM     :4文字の2-2時分データ(HHmm)を扱います。6.7.4.1 (2017/02/17)
489         * ・YMDHMS :14文字の4-2-2-2-2-2年月日時分秒データ(yyyyMMddHHmmss)を扱います。
490         * ・YMDHM  :12文字の4-2-2-2-2年月日時分データ(yyyyMMddHHmm)を扱います。
491         * ・YMDH   :10文字の4-2-2-2年月日時データ(yyyyMMddHH)を扱います。
492         * ・EEE    :曜日をデフォルトロケールで表示します。
493         *
494         * F付きは、フォーマットされた日付を返します。
495         * ・YMDF   :10文字の日付表現(yyyy/MM/dd)を扱います。
496         * ・YMD-F  :HTML5 type="date" の日付表現(yyyy-MM-dd)を扱います。8.0.1.2 (2021/11/19)
497         * ・Y2MDF  :8文字の日付表現(yy/MM/dd)を扱います。
498         * ・YMF    :7文字の日付表現(yyyy/MM)を扱います。
499         * ・YM-F   :HTML5 type="month"の日付表現(yyyy-MM)を扱います。8.0.1.2 (2021/11/19)
500         * ・HMSF   :8文字の時刻表現(HH:mm:ss)を扱います。
501         * ・HMF    :5文字の時刻表現(HH:mm)を扱います。6.7.4.1 (2017/02/17)
502         * ・YMDHMSF:19文字の日付表現(yyyy/MM/dd HH:mm:ss)を扱います。
503         * ・YMDHMF :17文字の日付表現(yyyy/MM/dd HH:mm)を扱います。
504         * ・YMDHM-F:17文字の日付表現(yyyy-MM-ddTHH:mm)を扱います。8.0.1.2 (2021/11/19)
505         * ・MDF    :5文字の月日表現(MM/dd)を扱います。
506         * ・MDEF   :5文字+曜日の月日表現(MM/dd(EEE))を扱います。
507         * ・MDHMF  :11文字の月日時分表現(MM/dd HH:mm)を扱います。 (7.0.0.1 (2018/10/09) 追加)
508         * ・MD2F   :漢字の月日表現(MM月dd日)を扱います。(5.5.5.2 追加)
509         * ・HM2F   :漢字の時分表現(HH時mm分)を扱います。(7.0.0.1 (2018/10/09) 追加)
510         * ・MDHM2F :漢字の月日時分表現(MM月dd日 HH時mm分)を扱います。(7.0.0.1 (2018/10/09) 追加)
511         * ・GYMDF  :和暦の年月日表現(GGGGyyyy年MM月dd日)を扱います。
512         * ・G2YMDF :和暦の日付表現(Gyyyy/MM/dd)を扱います。
513         * ・GYMF   :和暦の年月表現(GGGGyyyy年MM月)を扱います。
514         * ・GYF    :和暦の年表現(GGGGyyyy)を扱います。
515         *
516         * ・DIFF   :日付の差分を求めます。(7.0.1.1 (2018/10/22) 追加)
517         *               AA - BB を求め、CCの数値で単位を指定します。
518         *
519         * なお、上記以外のフォーマットを指定する場合は、XXXX部分に直接記述できます。(5.5.5.2 追加)
520         * ただし、基本的には、自由フォーマットは、エラーチェックがない為、使わないでください。
521         *
522         * 第二引数(prmA) AA は、基準となる日付を、yyyyMMdd形式で指定します。nullの場合は、現在時刻を使用します。
523         * 指定できる日付は、yyyyMMdd形式を推奨しますが、'/' , '-' , ' ' , ':' を削除して使います。
524         * 6桁の場合は、yyyyMM + 01 とし、8ケタの場合は、yyyyMMdd とし、14ケタ以上の場合は、前半14文字を
525         * yyyyMMddHHmmss として処理します。それ以外の桁数の場合は、エラーになります。
526         * たとえば、"2012/09/05 16:52:36" のようなフォーマットデータの場合、'/' , '-' , ' ' , ':' を削除して
527         * "20120905165236" に変換後、日付オブジェクトに変換されます。
528         *
529         * 第三引数(prmB) BB は、日付についての加減算処理を行うためのコマンドを指定します。
530         * nullの場合は、なにも加減算処理を行いません。
531         * ・SY :当年の最初の日付にセットします。(当年1月1日)。CC引数は、-N:N年前、0:当年(=SY)、N:N年後 6.9.2.1 (2018/03/12)
532         * ・SD :当月の最初の日付にセットします。(当月1日)。CC引数は、-N:N月前、0:当月(=SD)、N:N月後、-1:BSD と同じ、1:ASD と同じ
533         * ・SW :日付処理の週初め(月曜日)にセットします。日付は当日より前に移動します。CC引数は、-N:N週前、0:今週(=SW)、N:N週後
534         * ・SH :指定の最初の時にセットします。(分秒を0000にする)。CC引数は、時の倍数(4と指定すれば、4時間単位に前に戻る) 6.7.4.1 (2017/02/17)
535         * ・SM :指定の最初の分にセットします。(秒を00にする)。CC引数は、分の倍数(15と指定すれば、15分単位に前に戻る) 6.7.4.1 (2017/02/17)
536         * ・SS :指定の最初の秒にセットします。CC引数は、秒の倍数(15と指定すれば、15秒単位に前に戻る) 6.7.4.1 (2017/02/17)
537         * ・EY :当年の最後の日付にセットします。(当年年末)。CC引数は、-N:N年前、0:当年(=EY)、N:N年後 6.9.2.1 (2018/03/12)
538         * ・ED :当月の最後の日付にセットします。(当月月末)。CC引数は、-N:N月前、0:当月(=ED)、N:N月後、-1:BED と同じ、1:AED と同じ
539         * ・EW :日付処理の週末(日曜日)にセットします。日付は当日より後ろに移動します。CC引数は、-N:N週前、0:今週(=EW)、N:N週後
540         * ・EH :指定の次の時にセットします。(分秒を0000にした次の時)。CC引数は、時の倍数(4と指定すれば、4時間単位に前に進む) 6.7.4.1 (2017/02/17)
541         * ・EM :指定の次の分にセットします。(秒を00にした次の分)。CC引数は、分の倍数(15と指定すれば、15分単位に前に進む) 6.7.4.1 (2017/02/17)
542         * ・ES :指定の次の秒にセットします。CC引数は、秒の倍数(15と指定すれば、15秒単位に前に進む) 6.7.4.1 (2017/02/17)
543         * ・M1 ~ MXXX :月を指定の分だけ進めます。M1なら翌月、M6 なら半年後
544         * ・D1 ~ DXXX :日を指定の分だけ進めます。D1なら翌日、D200 なら200日後
545         * ・H1 ~ HXXX :時を指定の分だけ進めます。H1なら1時間後、H24 なら24時間後(5.5.5.6 (2012/08/31) 追加)
546         * ・MI  :分を指定の分だけ進めます。第四引数(intC) で、時間を指定します。(6.8.4.1 (2017/12/18) 追加)
547         * ・YMD :CC 引数のYMD表記の日付を加減算します。6.8.4.1 (2017/12/18) 追加
548         * ・HM  :CC 引数のHM表記の時刻を加減算します。6.8.4.1 (2017/12/18) 追加
549         * ・NO  :AA 引数がnullの場合、現在時刻ではなく空文字列にします。  7.0.1.3 (2018/11/12) 追加
550         * ・(有閑)BSD :先月の最初の日付にセットします。(先月1日)(5.5.5.2 追加)。SD -1 と同等
551         * ・(有閑)BED :先月の最後の日付にセットします。(先月月末)(5.5.5.2 追加)。ED -1 と同等
552         * ・(有閑)ASD :翌月の最初の日付にセットします。(翌月1日)(5.5.5.2 追加)。SD 1  と同等
553         * ・(有閑)AED :翌月の最後の日付にセットします。(翌月月末)(5.5.5.2 追加)。ED 1  と同等
554         *
555         * 7.0.1.1 (2018/10/22)
556         *   DATE.DIFF の場合、BB 引数は、日付データになります。AA-BB の関係です。
557         *
558         * CC 引数は、特別な処理で、BB 引数に対して、加算、減算のための数字を指定できます。(5.7.4.1 (2014/03/14) 追加)
559         * 従来は、BB 引数が、"H" , "D" , "M" の 1文字パラメータの場合のみ利用可能でした。
560         * これは、"H15" と指定するのと、"H" "15" と指定するのと同じ意味になります。
561         * 異なるのは、CC 引数も、(&#064;CC)指定で、リクエストパラメータが使用できます。
562         * 従来は、文字列として結合された状態でしか、BB 引数を渡せませんでしたが、この、CC 引数の
563         * 追加で、日付の加減算を、パラメータ指定できるようになります。
564         * 数字以外の文字が指定されたり、パラメータの解析結果が NULL の場合には、BB引数自体も無視されます。
565         * 注意点は、各 BB 引数に応じて、数字の意味が異なるという事です。
566         *
567         * HXXX,DXXX,MXXX 形式に、CC 引数を付けた場合は、XXX にさらに加算されます。
568         * prmB に、数字を使用した場合、(コマンドでない場合)にも、CC 引数は、加算されます。
569         *
570         * 7.0.1.1 (2018/10/22)
571         *   DATE.DIFF の場合、CC 引数は、差分の単位を指定するキーワードになります。AA-BB の結果を、
572         *   1:年 2:月 3:日 4:時 5:分 6:秒 に換算 して返します。端数は切り捨てで整数で返します。
573         *
574         * @og.rev 5.5.7.2 (2012/10/09) 新規作成
575         * @og.rev 5.6.1.1 (2013/02/08) prmB処理を、calendarCalc メソッドへ移動
576         * @og.rev 5.7.4.1 (2014/03/14) CC 引数を拡張
577         * @og.rev 6.4.3.3 (2016/03/04) Map#getOrDefault で対応する。
578         * @og.rev 7.0.1.1 (2018/10/22) DATE.DIFF 追加
579         * @og.rev 7.0.1.3 (2018/11/12) BB 引数に NO追加
580         * @og.rev 8.0.1.2 (2021/11/19) 日付フォーマット(key引数)に、\\n文字列が含まれる場合、変換しておきます。
581         * @og.rev 8.5.3.2 (2023/10/13) JDK21対応。 警告: [deprecation] LocaleのLocale(String,String,String)は推奨されません
582         *
583         * @param   key         フォーマットの予約語
584         * @param   prmA        基準となる日付(nullの場合は、処理時刻)
585         * @param   prmB        処理コマンド
586         * @param   intC        加減算処理を行うための数字。0 は、BB引数の従来計算のまま。
587         *
588         * @return   メッセージ情報
589         * @og.rtnNotNull
590         * @see         #getDateFormat( String )
591         * @see         #getDateFormat( String , String )
592         * @see         #getCalendar( String )                                          AA 引数 からカレンダオブジェクトを作成します。
593         * @see         #calendarCalc( Calendar , String , int )        BB 引数、CC 引数を元に、日付計算します。
594         */
595        public static String getDateFormat( final String key ,final String prmA ,final String prmB ,final int intC ) {
596                // 7.0.1.3 (2018/11/12) prmA が null の場合で、prmB が "NO" の場合は、ゼロ文字列を返します。
597                if( StringUtil.isEmpty( prmA ) && "NO".equalsIgnoreCase( prmB ) ) {
598                        return "";
599                }
600
601                // prmA が null の場合は、そのまま、現在時刻が使われます。
602                final Calendar now = getCalendar( prmA );
603
604                // 7.0.1.1 (2018/10/22) DATE.DIFF 追加
605                if( "DIFF".equalsIgnoreCase( key ) ) {
606                        return calendarDiff( now,prmB,intC );
607                }
608
609                // 5.6.1.1 (2013/02/08) getDateFormat( String ,String ,String ) から分離。
610                calendarCalc( now,prmB,intC );          // 5.7.4.1 (2014/03/14) CC 引数を拡張
611
612                // DATE_FORMAT に存在しないフォーマットを指定しても、エラーにしません。
613                // ただし、後処理でフォーマットエラーになる可能性は残ります。
614                // 6.4.3.3 (2016/03/04) Map#getOrDefault を使用します。
615                // 8.0.1.2 (2021/11/19) 日付フォーマット(key引数)に、\\n文字列が含まれる場合、変換しておきます。
616//              final String format = DATE_FORMAT.getOrDefault( key,key );              // 後ろの key は、値が null のときの初期値
617                final String format ;
618                if( key.contains( "\\n" ) ) {
619                        format = key.replace( "\\n" , "\n" );   // \\n を含むなら、DATE_FORMAT には存在しないハズ。
620                }
621                else {
622                        format = DATE_FORMAT.getOrDefault( key,key );
623                }
624
625                // 5.5.0.2 先頭Gの場合は和暦なのでformatterのLocaleを変更する
626                DateFormat formatter = null;
627                if( key.indexOf('G') == 0 ){
628//                      formatter = new SimpleDateFormat( format, new Locale("ja","JP","JP"));
629                        formatter = new SimpleDateFormat( format, JA_JP_JP);            // 8.5.3.2 (2023/10/13) JDK21対応
630                }
631                else{
632                        formatter = new SimpleDateFormat( format,Locale.JAPAN );
633                }
634
635                return formatter.format( now.getTime() );
636        }
637
638        /**
639         * 指定のの文字列から、カレンダオブジェクトを作成します。
640         * 基準となる日付に計算した結果を反映させます。
641         *
642         * prmB は、日付についての加減算処理を行うためのコマンドを指定します。
643         * ・数字       :HHMM 形式の時分です。
644         * ・H1 ~ HXXX :現在時刻に数字部分を+-します。分は0に初期化されます。
645         *
646         * @og.rev 8.5.4.2 (2024/01/12) Calendar インスタンスの作成方法変更( HybsDateUtil.newCalendar( String ) に移行
647         *
648         * @param   prmB        処理コマンド
649         * @return  カレンダオブジェクト
650         */
651        public static Calendar newCalendar( final String prmB ) {
652                final Calendar cal = Calendar.getInstance();
653
654                final boolean nowBase = prmB.charAt(0) == 'H' ;
655
656                if( nowBase ) {
657                        final int hour  = Integer.parseInt( prmB.substring( 1 ) );
658                        cal.add( Calendar.HOUR_OF_DAY,hour );
659                        cal.set( Calendar.MINUTE     ,0 );
660                        cal.set( Calendar.SECOND     ,0 );
661                }
662                else {
663                        final int hour  = Integer.parseInt( prmB.substring( 0,2 ) );
664                        final int minute        = Integer.parseInt( prmB.substring( 2,4 ) );
665                        cal.set( Calendar.HOUR_OF_DAY,hour );
666                        cal.set( Calendar.MINUTE     ,minute );
667                        cal.set( Calendar.SECOND     ,0 );
668                }
669
670                return cal;
671        }
672
673        /**
674         * 開始前設定値、または 終了後設定値の文字列から、オプション文字列を合成します。
675         * 基準となる日付に計算した結果を反映させます。
676         *
677         * CC引数の加減算パラメータは、0 です。
678         *
679         * @og.rev 5.7.4.1 (2014/03/14) CC 引数を拡張するため、旧メソッドを再現しておきます。
680         *
681         * @param   now     基準となる日付(Calendarオブジェクト)
682         * @param   prmB        処理コマンド
683         */
684        public static void calendarCalc( final Calendar now,final String prmB ) {
685                calendarCalc( now,prmB,0 );
686        }
687
688        /**
689         * 開始前設定値、または 終了後設定値の文字列から、オプション文字列を合成します。
690         * 基準となる日付に計算した結果を反映させます。
691         *
692         * prmB は、日付についての加減算処理を行うためのコマンドを指定します。
693         * ・SY :当年の最初の日付にセットします。(当年1月1日)。CC引数は、-N:N年前、0:当年(=SY)、N:N年後 6.9.2.1 (2018/03/12)
694         * ・SD :当月の最初の日付にセットします。(当月1日)。CC引数は、-N:N月前、0:当月(=SD)、N:N月後、-1:BSD と同じ、1:ASD と同じ
695         * ・SW :日付処理の週初め(月曜日)にセットします。日付は当日より前に移動します。CC引数は、-N:N週前、0:今週(=SW)、N:N週後
696         * ・SH :指定の最初の時にセットします。(分秒を0000にする)。CC引数は、時の倍数(4と指定すれば、4時間単位に前に戻る) 6.7.4.1 (2017/02/17)
697         * ・SM :指定の最初の分にセットします。(秒を00にする)。CC引数は、分の倍数(15と指定すれば、15分単位に前に戻る) 6.7.4.1 (2017/02/17)
698         * ・SS :指定の最初の秒にセットします。CC引数は、秒の倍数(15と指定すれば、15秒単位に前に戻る) 6.7.4.1 (2017/02/17)
699         * ・EY :当年の最後の日付にセットします。(当年年末)。CC引数は、-N:N年前、0:当年(=EY)、N:N年後 6.9.2.1 (2018/03/12)
700         * ・ED :当月の最後の日付にセットします。(当月月末)。CC引数は、-N:N月前、0:当月(=ED)、N:N月後、-1:BED と同じ、1:AED と同じ
701         * ・EW :日付処理の週末(日曜日)にセットします。日付は当日より後ろに移動します。CC引数は、-N:N週前、0:今週(=EW)、N:N週後
702         * ・EH :指定の次の時にセットします。(分秒を0000にした次の時)。CC引数は、時の倍数(4と指定すれば、4時間単位に前に進む) 6.7.4.1 (2017/02/17)
703         * ・EM :指定の次の分にセットします。(秒を00にした次の分)。CC引数は、分の倍数(15と指定すれば、15分単位に前に進む) 6.7.4.1 (2017/02/17)
704         * ・ES :指定の次の秒にセットします。CC引数は、秒の倍数(15と指定すれば、15秒単位に前に進む) 6.7.4.1 (2017/02/17)
705         * ・M1 ~ MXXX :月を指定の分だけ進めます。M1なら翌月、M6 なら半年後
706         * ・D1 ~ DXXX :日を指定の分だけ進めます。D1なら翌日、D200 なら200日後
707         * ・H1 ~ HXXX :時を指定の分だけ進めます。H1なら1時間後、H24 なら24時間後(5.5.5.6 (2012/08/31) 追加)
708         * ・MI  :分を指定の分だけ進めます。第四引数(intC) で、時間を指定します。(6.8.4.1 (2017/12/18) 追加)
709         * ・YMD :CC 引数のYMD表記の日付を加減算します。6.8.4.1 (2017/12/18) 追加
710         * ・HM  :CC 引数のHM表記の時刻を加減算します。6.8.4.1 (2017/12/18) 追加
711         * ・NO  :AA 引数がnullの場合、現在時刻ではなく空文字列にします。  7.0.1.3 (2018/11/12) 追加
712         * ・(有閑)BSD :先月の最初の日付にセットします。(先月1日)(5.5.5.2 追加)。SD-1 と同等
713         * ・(有閑)BED :先月の最後の日付にセットします。(先月月末)(5.5.5.2 追加)。ED-1 と同等
714         * ・(有閑)ASD :翌月の最初の日付にセットします。(翌月1日)(5.5.5.2 追加)。SD1  と同等
715         * ・(有閑)AED :翌月の最後の日付にセットします。(翌月月末)(5.5.5.2 追加)。ED1  と同等
716         * ・数字:日を指定の分だけ進めます。D1 ~ DXXX の簡略系
717         *
718         * CC 引数は、特別な処理で、BB 引数に対して、加算、減算のための数字を指定できます。(5.7.4.1 (2014/03/14) 追加)
719         * HXXX,DXXX,MXXX 形式に、CC 引数を付けた場合は、XXX にさらに加算されます。
720         * prmB に、数字を使用した場合、(コマンドでない場合)にも、CC 引数は、加算されます。
721         * 6.8.4.1 (2017/12/18) BB 引数に、HM を指定した場合、時分のデータを加算します。この場合、時に関しては、24時間を越える
722         * 場合は、日付に加算されます。例えば、翌朝の6時を指定する場合、3000 と指定することで、1日+0600 となります。
723         *
724         * @og.rev 5.6.1.1 (2013/02/08) getDateFormat( String ,String ,String ) から分離。
725         * @og.rev 5.7.4.1 (2014/03/14) H1 ~ HXXX :時を指定の分だけ進める処理が実装されていなかった。
726         * @og.rev 5.7.4.1 (2014/03/14) CC 引数追加
727         * @og.rev 6.7.4.1 (2017/02/17) SH,SM,SS,EH,EM,ES 追加
728         * @og.rev 6.8.4.1 (2017/12/18) YMD , HM , MI 追加
729         * @og.rev 6.9.2.1 (2018/03/12) 年関連の機能(Y4,SY,EY)追加
730         * @og.rev 7.0.1.3 (2018/11/12) BB 引数に NO追加
731         *
732         * @param   now     基準となる日付(Calendarオブジェクト)
733         * @param   prmB        処理コマンド
734         * @param   intC        加減算処理を行うための数字。0 は、BB引数の従来計算のまま。
735         */
736        public static void calendarCalc( final Calendar now , final String prmB , final int intC ) {
737
738                // 基準は、intC == 0 の場合
739                if( prmB != null && prmB.length() > 0 ) {                               // 6.9.2.1 (2018/03/12) 年関連の機能(Y4,SY,EY)追加
740                        if( "SY".equals( prmB ) ) {                                                     // (当年1月1日)
741                                if( intC != 0 ) { now.add( Calendar.YEAR,intC ); }
742                                now.set( Calendar.DAY_OF_YEAR,1 );                              // 現在の年の何日目かで指定します。
743                        }
744                        else if( "EY".equals( prmB ) ) {                                        // (当年年末)
745                                if( intC != 0 ) { now.add( Calendar.YEAR,intC ); }
746                                now.set( Calendar.MONTH,11 );                                   // 月は、0~11 の値で指定。
747                                now.set( Calendar.DATE,now.getActualMaximum( Calendar.DATE ) );
748                        }
749                        else if( "SD".equals( prmB ) ) {                                                        // (当月1日)
750                                if( intC != 0 ) { now.add( Calendar.MONTH,intC ); }     // 5.7.4.1 (2014/03/14) CC 引数追加
751                                now.set( Calendar.DATE,1 );
752                        }
753                        else if( "ED".equals( prmB ) ) {                                        // (当月月末)
754                                if( intC != 0 ) { now.add( Calendar.MONTH,intC ); }     // 5.7.4.1 (2014/03/14) CC 引数追加
755                                now.set( Calendar.DATE,now.getActualMaximum( Calendar.DATE ) );
756                        }
757                        else if( "SH".equals( prmB ) ) {                                        // (時戻し) 6.7.4.1 (2017/02/17) SH,SM,SS,EH,EM,ES 追加
758                                // 6.9.8.0 (2018/05/28) FindBugs:整数の除算の結果を double または float にキャストしている
759//                              final int hh = intC == 0 ? 0 : (int)Math.floor( now.get( Calendar.HOUR_OF_DAY ) / intC ) * intC ;       // 切捨て
760                                final int hh = intC == 0 ? 0 : ( now.get( Calendar.HOUR_OF_DAY ) / intC ) * intC ;      // 切捨て
761                                now.set( Calendar.HOUR_OF_DAY,hh );
762                                now.set( Calendar.MINUTE     ,0 );
763                                now.set( Calendar.SECOND     ,0 );
764                                now.set( Calendar.MILLISECOND,0 );
765                        }
766                        else if( "SM".equals( prmB ) ) {                                        // (分戻し) 6.7.4.1 (2017/02/17) SH,SM,SS,EH,EM,ES 追加
767                                // 6.9.8.0 (2018/05/28) FindBugs:整数の除算の結果を double または float にキャストしている
768//                              final int mm = intC == 0 ? 0 : (int)Math.floor( now.get( Calendar.MINUTE ) / intC ) * intC ;    // 切捨て
769                                final int mm = intC == 0 ? 0 : ( now.get( Calendar.MINUTE ) / intC ) * intC ;   // 切捨て
770                                now.set( Calendar.MINUTE     ,mm );
771                                now.set( Calendar.SECOND     ,0 );
772                                now.set( Calendar.MILLISECOND,0 );
773                        }
774                        else if( "SS".equals( prmB ) ) {                                        // (秒戻し) 6.7.4.1 (2017/02/17) SH,SM,SS,EH,EM,ES 追加
775                                // 6.9.8.0 (2018/05/28) FindBugs:整数の除算の結果を double または float にキャストしている
776//                              final int ss = intC == 0 ? 0 : (int)Math.floor( now.get( Calendar.SECOND ) / intC ) * intC ;    // 切捨て
777                                final int ss = intC == 0 ? 0 : ( now.get( Calendar.SECOND ) / intC ) * intC ;   // 切捨て
778                                now.set( Calendar.SECOND     ,ss );
779                                now.set( Calendar.MILLISECOND,0 );
780                        }
781                        else if( "EH".equals( prmB ) ) {                                        // (時進め) 6.7.4.1 (2017/02/17) SH,SM,SS,EH,EM,ES 追加
782                                final int hh = intC == 0 ? 0 : (int)Math.ceil( (double)now.get( Calendar.HOUR_OF_DAY ) / intC ) * intC ;        // 切り上げ
783                                if( hh == 0 || hh >= 24 ) {
784                                        now.add( Calendar.DATE       ,1 );                      // 日を加算
785                                        now.set( Calendar.HOUR_OF_DAY,0 );
786                                }
787                                else {
788                                        now.set( Calendar.HOUR_OF_DAY,hh );
789                                }
790                                now.set( Calendar.MINUTE     ,0 );
791                                now.set( Calendar.SECOND     ,0 );
792                                now.set( Calendar.MILLISECOND,0 );
793                        }
794                        else if( "EM".equals( prmB ) ) {                                        // (分進め) 6.7.4.1 (2017/02/17) SH,SM,SS,EH,EM,ES 追加
795                                final int mm = intC == 0 ? 0 : (int)Math.ceil( (double)now.get( Calendar.MINUTE ) / intC ) * intC ;             // 切切り上げ
796                                if( mm == 0 || mm >= 60 ) {
797                                        now.add( Calendar.HOUR_OF_DAY,1 );                      // 時を加算
798                                        now.set( Calendar.MINUTE     ,0 );
799                                }
800                                else {
801                                        now.set( Calendar.MINUTE,mm );
802                                }
803                                now.set( Calendar.SECOND     ,0 );
804                                now.set( Calendar.MILLISECOND,0 );
805                        }
806                        else if( "ES".equals( prmB ) ) {                                        // (秒進め) 6.7.4.1 (2017/02/17) SH,SM,SS,EH,EM,ES 追加
807                                final int ss = intC == 0 ? 0 : (int)Math.ceil( (double)now.get( Calendar.SECOND ) / intC ) * intC ;     // 切り上げ
808                                if( ss == 0 || ss >= 60 ) {
809                                        now.add( Calendar.MINUTE ,1 );                          // 分を加算
810                                        now.set( Calendar.SECOND ,0 );
811                                }
812                                else {
813                                        now.set( Calendar.SECOND,ss );
814                                }
815                                now.set( Calendar.MILLISECOND,0 );
816                        }
817                        else if( "BSD".equals( prmB ) ) {                                       // (先月1日)
818                                // 5.7.4.1 (2014/03/14) CC 引数追加
819                                now.add( Calendar.MONTH,intC-1 ); now.set( Calendar.DATE,1 );
820                        }
821                        else if( "BED".equals( prmB ) ) {                                       // (先月月末)
822                                // 5.7.4.1 (2014/03/14) CC 引数追加
823                                now.add( Calendar.MONTH,intC-1 ); now.set( Calendar.DATE,now.getActualMaximum( Calendar.DATE ) );
824                        }
825                        else if( "ASD".equals( prmB ) ) {                                       // (翌月1日)
826                                // 5.7.4.1 (2014/03/14) CC 引数追加
827                                now.add( Calendar.MONTH,intC+1 ); now.set( Calendar.DATE,1 );
828                        }
829                        else if( "AED".equals( prmB ) ) {                                       // (翌月月末)
830                                // 5.7.4.1 (2014/03/14) CC 引数追加
831                                now.add( Calendar.MONTH,intC+1 ); now.set( Calendar.DATE,now.getActualMaximum( Calendar.DATE ) );
832                        }
833                        else if( "SW".equals( prmB ) ) {                                        // 週初め(月曜日)セット
834                                // 5.7.4.1 (2014/03/14) CC 引数追加
835                                if( intC != 0 ) { now.add( Calendar.DATE,intC*7 ); }    // まず、基準の日付を週単位で加減算する。
836
837                                // 日付型文字列入力データの開始日を月曜日にセットします。
838                                // SUNDAY=1 , MONDAY=2 になります。月曜日との差だけ、前に戻します。
839                                // 指定日が日曜日の場合は、月曜日まで戻します。
840
841                                final int shu = now.get( Calendar.DAY_OF_WEEK ) - Calendar.MONDAY ;
842
843                                if(      shu > 0 ) { now.add( Calendar.DATE, -shu ); }
844                                else if( shu < 0 ) { now.add( Calendar.DATE, -6 );   }
845                        }
846                        else if( "EW".equals( prmB ) ) {                                        // 週末(日曜日)にセット
847                                // 5.7.4.1 (2014/03/14) CC 引数追加
848                                if( intC != 0 ) { now.add( Calendar.DATE,intC*7 ); }    // まず、基準の日付を週単位で加減算する。
849
850                                // 日付型文字列入力データの終了日を日曜日にセットします。
851                                // SUNDAY=1 , MONDAY=2 になります。日曜日になるように、先に進めます。
852                                final int shu = now.get( Calendar.DAY_OF_WEEK ) ;
853                                if( shu != Calendar.SUNDAY ) { now.add( Calendar.DATE, 8-shu ); }
854                        }
855                        // 6.8.4.1 (2017/12/18) YMD 追加(年月日の繰り上げ表記を加味した加算)
856                        else if( "YMD".equals( prmB ) ) {
857                                final int year  = intC / 10_000;        // 8.5.4.2 (2024/01/12) PMD 7.0.0 UseUnderscoresInNumericLiterals
858                                final int month = ( intC / 100 ) % 100;
859                                final int date  = intC % 100;
860
861                                now.add( Calendar.YEAR  , year );
862                                now.add( Calendar.MONTH , month );
863                                now.add( Calendar.DATE  , date );
864                        }
865                        // 6.8.4.1 (2017/12/18) HM 追加(時分の24時間表記を加味した加算)
866                        else if( "HM".equals( prmB ) ) {                        // 注意:prmB.charAt(0) == 'H' より先に判定しておきます。
867                                final int hour   = intC / 100;
868                                final int minute = intC % 100;
869
870                                now.add( Calendar.HOUR_OF_DAY   , hour );
871                                now.add( Calendar.MINUTE                , minute );
872                        }
873                        // 6.8.4.1 (2017/12/18) HM 分を指定の分だけ進めます。
874                        else if( "MI".equals( prmB ) ) {                        // 注意:prmB.charAt(0) == 'M' より先に判定しておきます。
875                                now.add( Calendar.MINUTE                , intC );
876                        }
877                        // 7.0.1.3 (2018/11/12) AA 引数がnullの場合、現在時刻ではなく空文字列にします。
878                        else if( "NO".equals( prmB ) ) {
879                                ;       // 何もしません。                                      // 8.5.4.2 (2024/01/12) 空行を明示する方法       //NOPMD
880                        }
881                        // 6.9.2.1 (2018/03/12) Y1 ~ YXXX :年を指定の分だけ進める処理
882                        else if( prmB.charAt(0) == 'Y' ) {
883                                int year = intC ;
884                                if( prmB.length() > 1 ) { year += Integer.parseInt( prmB.substring( 1 ) ); }
885                                now.add( Calendar.YEAR , year );
886                        }
887                        else if( prmB.charAt(0) == 'H' ) {                      // 6.1.0.0 (2014/12/26) refactoring
888                                int hour = intC ;
889                                if( prmB.length() > 1 ) { hour += Integer.parseInt( prmB.substring( 1 ) ); }
890                                now.add( Calendar.HOUR_OF_DAY , hour );
891                        }
892                        else if( prmB.charAt(0) == 'D' ) {                      // 6.1.0.0 (2014/12/26) refactoring
893                                int day = intC ;
894                                if( prmB.length() > 1 ) { day += Integer.parseInt( prmB.substring( 1 ) ); }
895                                now.add( Calendar.DATE, day );
896                        }
897                        else if( prmB.charAt(0) == 'M' ) {                      // 6.1.0.0 (2014/12/26) refactoring
898                                int month = intC ;
899                                if( prmB.length() > 1 ) { month += Integer.parseInt( prmB.substring( 1 ) ); }
900                                now.add( Calendar.MONTH , month );
901                        }
902                        else {
903                                // 上記のパターン以外は、数字(加減算する日数)なので、変換できなければ、フォーマットエラー
904                                try {
905                                        final int day = Integer.parseInt( prmB ) + intC ;       // 5.7.4.1 (2014/03/14) CC 引数追加
906                                        now.add( Calendar.DATE, day );
907                                }
908                                catch( final NumberFormatException ex ) {
909                                        final String errMsg = "日付変数パラメータに、不正な値が指定されました。以下の中から指定しなおしてください。"
910//                                                              + "指定可能:[SD,ED,SW,SH,SM,SS,EW,EH,EM,ES,H1~HXXX,D1~DXXX,M1~MXXX,HMS,BSD,BED,ASD,AED]"
911                                                                + "指定可能:[SY,SD,SW,SH,SM,SS,EY,ED,EW,EH,EM,ES,M1~MXXX,D1~DXXX,H1~HXXX,MI,YMD,HM,NO]"
912                                                                + " prmB=[" + prmB + "]" ;
913                                        throw new OgRuntimeException( errMsg,ex );
914                                }
915                        }
916                }
917        }
918
919        /**
920         * 日付の差分を求めます。
921         * timeA - timeB を求め、intCの数値で単位を指定します。
922         *
923         * intC は、1:年 2:月 3:日 4:時 5:分 6:秒 に換算した単位での差分になります。
924         * 端数は出ません。
925         *
926         * @og.rev 7.0.1.1 (2018/10/22) DATE.DIFF 追加
927         * @og.rev 7.2.9.4 (2020/11/20) spotbugs:switch 文の2つの case のために同じコードを使用しているメソッド
928         * @og.rev 8.5.5.1 (2024/02/29) switch式の使用
929         *
930         * @param   timeA   基準となる日付(Calendarオブジェクト)
931         * @param   prmB        日付データになります。AA-BB の関係です。
932         * @param   intC        差分の単位を指定するキーワード(1:年 2:月 3:日 4:時 5:分 6:秒 に換算)
933         * @return      指定の時間の差(timeA - timeB マイナスもある)
934         */
935        public static String calendarDiff( final Calendar timeA , final String prmB , final int intC ) {
936                final Calendar timeB = getCalendar( prmB );
937
938                final int diff ;
939                if( intC == 1 ) {                       // 差分年
940                        diff = timeA.get( Calendar.YEAR ) - timeB.get( Calendar.YEAR );
941                }
942                else if( intC == 2 ) {          // 差分月数
943                        diff = ( timeA.get( Calendar.YEAR  ) - timeB.get( Calendar.YEAR  ) ) * 12
944                                +  ( timeA.get( Calendar.MONTH ) - timeB.get( Calendar.MONTH ) ) ;
945
946        //              // 月の計算ロジック( http://javatechnology.net/java/date-diff-month/ )
947        //              // マイナス計算も必要なので、今回は不採用
948        //              timeA.set( Calendar.DATE, 1 );          // 端数を無視するため、1日にセットします。
949        //              timeB.set( Calendar.DATE, 1 );          // 同上
950
951        //              int count = 0;
952        //              while( timeA.before( timeB ) ) {
953        //                      timeA.add( Calendar.MONTH, 1 );
954        //                      count++;
955        //              }
956        //              diff = count;
957                }
958                else {
959                        final long diffSec = timeA.getTimeInMillis() - timeB.getTimeInMillis() ;        // 時間の差(ミリ秒)
960
961                        // 8.5.5.1 (2024/02/29) switch式の使用
962//                      switch( intC ) {
963//      //                      case 3:  diff = (int)( diffSec / DD );  break;          // 日単位                  // 7.2.9.4 (2020/11/20)
964//                              case 4:  diff = (int)( diffSec / HH );  break;          // 時単位
965//                              case 5:  diff = (int)( diffSec / MM );  break;          // 分単位
966//                              case 6:  diff = (int)( diffSec / SS );  break;          // 秒単位
967//                              default: diff = (int)( diffSec / DD );  break;          // (初期値)日単位
968//                      }
969                        diff = switch( intC ) {
970        //                      case 3 -> (int)( diffSec / DD );                // 日単位                  // 7.2.9.4 (2020/11/20)
971                                case 4 -> (int)( diffSec / HH );                // 時単位
972                                case 5 -> (int)( diffSec / MM );                // 分単位
973                                case 6 -> (int)( diffSec / SS );                // 秒単位
974                                default -> (int)( diffSec / DD );               // (初期値)日単位
975                        };
976                }
977
978                return Integer.toString( diff ) ;
979        }
980
981        /**
982         * 指定の引数の日付け文字列より、カレンダオブジェクトを作成します。
983         * 引数は、数字以外の文字を削除した状態に変換後、処理に回します。
984         * 不要な文字を削除した状態で、8文字以上になるように指定してください。
985         * 例外的に、6文字の場合は、yyyyMM01 とみなして、"01" 文字列を付与します。
986         * 引数に null を指定すると、現在時刻のカレンダを返します。
987         * それ以外のデータで、8ケタ以下の場合は、RuntimeException が発生します。
988         * 8ケタ以上14ケタ未満の場合は、8ケタ分を、年月日に分離したカレンダ
989         * オブジェクトを作成します。14ケタ以上で初めて、時分秒を含むカレンダ
990         * を作成します。
991         *
992         * @og.rev 5.5.7.2 (2012/10/09) 新規作成
993         * @og.rev 5.5.8.2 (2012/11/09) value の判定に、null と ゼロ文字列を判定する。
994         *
995         * @param value 日付け文字列
996         *
997         * @return      カレンダオブジェクト(引数がnullの場合は、現在時刻)
998         * @og.rtnNotNull
999         */
1000        public static Calendar getCalendar( final String value ) {
1001                final Calendar cal = Calendar.getInstance();
1002
1003                if( value == null || value.isEmpty() ) { return cal; }          // 5.5.8.2 (2012/11/09) null と ゼロ文字列を判定する。
1004
1005                // 日付表記に不要な文字を削除します。
1006                String dateStr = parseNumber( value ) ;
1007
1008                if( dateStr.length() == 6 ) { dateStr = dateStr + "01"; }       // yyyyMM01 形式に無理やり合わせる。
1009                else if( dateStr.length() < 8 ) {
1010                        final String errMsg = "日付指定パラメータに、不正な値が指定されました。value=[" + value + "]" ;
1011                        throw new OgRuntimeException( errMsg );
1012                }
1013
1014                cal.clear();    // 日付文字列が存在するので、カレンダをリセット
1015
1016                final int year   = Integer.parseInt( dateStr.substring( 0,4 ) );
1017                final int month  = Integer.parseInt( dateStr.substring( 4,6 ) ) - 1;
1018                final int date   = Integer.parseInt( dateStr.substring( 6,8 ) );
1019
1020                // 6.3.9.0 (2015/11/06) Use one line for each declaration, it enhances code readability.(PMD)
1021                int hour  =0;
1022                int minute=0;
1023                int second=0;
1024                if( dateStr.length() >= 14 ) {
1025                        hour   = Integer.parseInt( dateStr.substring( 8,10 ) );
1026                        minute = Integer.parseInt( dateStr.substring( 10,12 ) );
1027                        second = Integer.parseInt( dateStr.substring( 12,14 ) );
1028                }
1029
1030                cal.set( year,month,date,hour,minute,second );
1031
1032                return cal;
1033        }
1034
1035        /**
1036         * 指定の引数の日付け文字列(yyyyMMdd)より、日付を加算して返します。
1037         * マイナスを与えると、減算します。
1038         * 日付以上の精度の文字列を渡しても、日付のみの計算となります。
1039         * 結果は、引数の日付フォーマットとは全く別で、yyyyMMdd の8文字形式になります。
1040         * 引数に null を渡すと、実行時の日付をベースとして処理します。
1041         *
1042         * @og.rev 5.5.7.2 (2012/10/09) 新規作成
1043         *
1044         * @param baseDate 日付け文字列(yyyyMMdd)
1045         * @param plus     加算する日数(過去にするにはマイナス値を指定する)
1046         *
1047         * @return      結果の日付(yyyyMMdd)
1048         * @og.rtnNotNull
1049         */
1050        public static String getDatePlus( final String baseDate,final int plus ) {
1051                final Calendar cal = getCalendar( baseDate );
1052                cal.add( Calendar.DATE,plus );
1053
1054                return DateSet.getDate( cal.getTimeInMillis() , "yyyyMMdd" );
1055        }
1056
1057        /**
1058         * 現在の月に、指定の月数をプラスした日付文字列を返します。
1059         * 日付文字列のフォーマットは、"yyyyMM" です。
1060         * 指定する月数にマイナスを指定すると、減算できます。
1061         *
1062         * @og.rev 5.5.7.2 (2012/10/09) 新規作成
1063         *
1064         * @param baseDate 日付け文字列(yyyyMM)
1065         * @param plus     加算する月数(過去にするにはマイナス値を指定する)
1066         *
1067         * @return      指定の月数をプラスした日付文字列(yyyyMM)
1068         * @og.rtnNotNull
1069         */
1070        public static String getMonthPlus( final String baseDate,final int plus ) {
1071                final Calendar cal = getCalendar( baseDate );
1072                cal.set( Calendar.DATE, 1 );            // 当月の 1 日に設定
1073                cal.add( Calendar.MONTH , plus );
1074
1075                return DateSet.getDate( cal.getTimeInMillis() , "yyyyMM" );
1076        }
1077
1078        /**
1079         * 指定の引数の日付け文字列(yyyyMMdd、yyyyMMddHHmmss)に、日付を加算して返します。
1080         * マイナスを与えると、減算します。
1081         *
1082         * 指定する日付には、単位を付与することが可能です。
1083         * 単位は、yyyyMMddHHmmss 形式の1文字を指定します。大文字、小文字も識別します。
1084         * plus="5M" とすれば、5か月、plus="5d"  とすれば、5日 追加します。
1085         * plus に単位を付けない場合は、tani に指定の単位を使います。
1086         * plus そのものが、null か、isEmpty の場合は、加算は、1 になります。
1087         *
1088         * baseDate 文字列を日付文字列に変換後、Calendar で計算し、結果を、format 形式に変換します。
1089         * 引数に null を渡すと、実行時の日付をベースとして処理します。
1090         *
1091         * @og.rev 5.6.1.0 (2013/02/01) 新規作成
1092         * @og.rev 8.5.5.1 (2024/02/29) switch式の使用
1093         *
1094         * @param baseDate 日付け文字列(yyyyMMdd、yyyyMMddHHmmss 形式の日付文字列)
1095         * @param plus     加算する日数(日付単位を含む。単位は、y,M,d,H,m,s の文字で、大文字小文字の区別があります)
1096         * @param defTani  日付単位が未指定の場合の初期単位('y','M','d','H','m','s' のどれか)
1097         * @param format   返す日付文字列のフォーマット(yyyyMMdd、yyyyMMddHHmmss)
1098         *
1099         * @return      結果の日付(yyyyMMdd)
1100         * @throws      NumberFormatException 加算する日数の単位が('y','M','d','H','m','s')以外の場合。
1101         * @og.rtnNotNull
1102         */
1103        public static String getDatePlus( final String baseDate,final String plus,final int defTani,final String format ) {
1104
1105                int addSu = 1;                          // 初期値(plus が null や Empty の場合は、+1となる)
1106                int tani  = defTani;
1107
1108                if( plus != null && !plus.isEmpty() ) {
1109                        boolean flag = true;    // 日付単位を持っているかどうか。持っている場合は、true
1110                        final char ch = plus.charAt( plus.length()-1 );         // 最後の一文字を取得(単位か、数字本体)
1111                        // 8.5.5.1 (2024/02/29) switch式の使用
1112//                      switch( ch ) {
1113//                              case 'y' : tani = Calendar.YEAR;                break ;
1114//                              case 'M' : tani = Calendar.MONTH;               break ;
1115//                              case 'd' : tani = Calendar.DATE;                break ;
1116//                              case 'H' : tani = Calendar.HOUR_OF_DAY; break ;
1117//                              case 'm' : tani = Calendar.MINUTE;              break ;
1118//                              case 's' : tani = Calendar.SECOND;              break ;
1119//                              default  : flag = false;        break ;         // 日付単位を持っていない。
1120//                      }
1121                        tani = switch( ch ) {
1122                                case 'y' -> Calendar.YEAR;
1123                                case 'M' -> Calendar.MONTH;
1124                                case 'd' -> Calendar.DATE;
1125                                case 'H' -> Calendar.HOUR_OF_DAY;
1126                                case 'm' -> Calendar.MINUTE;
1127                                case 's' -> Calendar.SECOND;
1128                                default  -> { flag = false; yield tani; }               // 日付単位を持っていない。
1129                        };
1130                        if( flag ) {
1131                                addSu = Integer.parseInt( plus.substring( 0,plus.length()-1 ) );        // 日付単位 あり
1132                        }
1133                        else {
1134                                addSu = Integer.parseInt( plus ) ;                                                                      // 日付単位 なし
1135                        }
1136                }
1137
1138                final Calendar cal = getCalendar( baseDate );
1139                cal.add( tani,addSu );
1140
1141                return DateSet.getDate( cal.getTimeInMillis() , format );
1142        }
1143
1144        /**
1145         * 指定の日付文字列を指定の形式の文字列に変換します。
1146         * 入力の日付文字列は、"yyyyMMddHHmmss" または、 "yyyyMMdd" です。
1147         * 記号が入っている場合でも、一応処理します。
1148         * 指定のフォーマットも、同様に、yyyyMMddHHmmss 形式で指定します。
1149         *
1150         * @og.rev 7.0.1.2 (2018/11/04) 新規登録
1151         *
1152         * @param ymd           日付け文字列(yyyyMM)
1153         * @param format        加算する月数(過去にするにはマイナス値を指定する)
1154         *
1155         * @return      指定の日付文字列を指定の形式の文字列に変換
1156         * @og.rtnNotNull
1157         */
1158        public static String toYmd( final String ymd,final String format ) {
1159                final Calendar cal = getCalendar( ymd );
1160
1161                return DateSet.getDate( cal.getTimeInMillis() , format );
1162        }
1163
1164        /**
1165         * 指定の年月文字列のFrom-Toを、文字列の配列で返します。
1166         * "yyyyMM" 形式の範囲内の文字列配列を作成します。
1167         *
1168         * ※ endがnullの場合は、beginそのものを配列にセットして返します。
1169         *    つまり、1件のみの配列になります。
1170         * ※ end == beginの場合は、配列は、ゼロ個の配列になります。
1171         *
1172         * @og.rev 7.0.5.0 (2019/09/09) 新規追加
1173         *
1174         * @param begin         開始の年月(含む)
1175         * @param end           終了の年月(含まない)
1176         * @param step          加算する月数
1177         *
1178         * @return      指定の年月文字列のFrom-Toを、文字列の配列で返す
1179         * @og.rtnNotNull
1180         */
1181        public static String[] stepYM( final String begin,final String end,final int step ) {
1182                final List<String> list = new ArrayList<>();
1183
1184                if( StringUtil.isNull( end ) ) {
1185                        list.add( begin );
1186                }
1187                else {
1188                        final Calendar timeA = getCalendar( begin );
1189                        final Calendar timeB = getCalendar( end );
1190
1191                        while( timeA.before( timeB ) ) {
1192                                list.add( DateSet.getDate( timeA.getTimeInMillis() , "yyyyMM" ) );
1193                                timeA.add( Calendar.MONTH, 1 );
1194                        }
1195                }
1196
1197//               return list.toArray( new String[list.size()] ) ;
1198                 return list.toArray( new String[0] ) ; // 8.5.4.2 (2024/01/12) PMD 7.0.0 OptimizableToArrayCall 対応
1199        }
1200}