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