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.hayabusa.resource;
017
018import java.util.Calendar;
019import java.util.Map;
020import java.util.SortedMap;
021import java.util.TreeMap ;
022import java.util.BitSet ;
023
024import org.opengion.fukurou.util.HybsDateUtil;                                          // 6.4.2.0 (2016/01/29)
025import org.opengion.hayabusa.common.HybsSystemException;
026import static org.opengion.fukurou.system.HybsConst.CR ;                                // 6.1.0.0 (2014/12/26)
027import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;      // 6.1.0.0 (2014/12/26) refactoring
028
029/**
030 * 事業所(CDJGS) 毎の休日カレンダデータオブジェクトです。
031 *
032 * カレンダデータは、指定の事業所に関して、すべての休日情報を持っています。
033 * 元のカレンダテーブル(GE13)の 1日(DY1)~31日(DY31)までの日付け欄に対して、
034 * 休日日付けの 年月日 に対する、休日かどうかを判断できるだけの情報を保持します。
035 * 具体的には、休日の年月日に対する List を持つことになります。
036 * このクラスは、パッケージプライベートになっています。このオブジェクトを作成するのは、
037 * CalendarFactory#getCalendarData( String ) で行います。引数は、事業所コード(cdjgs)です。
038 * このカレンダオブジェクトを使用するには、事業所カレンダテーブル(GE13) を使用する
039 * ことを許可しておく必要があります。
040 * 許可は、システムパラメータ の USE_CALENDAR_DATABASE 属性を true に
041 * 設定します(初期値は、互換性優先の false です。)
042 * この、カレンダテーブルは、GE13 固定です。他のテーブルを使用する場合は、
043 * ビュー等を作成する必要があります。
044 * カレンダテーブル は、通常 DEFAULT DBIDを使用しますが、RESOURCE_CALENDAR_DBID
045 * を設定することで、他のデータベースから読み取ることが可能になります。
046 *
047 * @og.rev 3.6.0.0 (2004/09/17) 新規作成
048 * @og.group リソース管理
049 *
050 * @version  4.0
051 * @author   Kazuhiko Hasegawa
052 * @since    JDK5.0,
053 */
054// 8.5.5.1 (2024/02/29) spotbugs CT_CONSTRUCTOR_THROW(コンストラクタで、Excweptionを出さない) class を final にすれば、警告は消える。
055// class CalendarDBData implements CalendarData {
056final class CalendarDBData implements CalendarData {
057        private final SortedMap<String,BitSet> ymMap  = new TreeMap<>() ;       // 休日日付けデータを年月をキーに持ちます。
058
059        /**
060         * コンストラクタ
061         *
062         * 配列文字列のデータを元に、CalendarDBDataオブジェクトを構築します。
063         * このコンストラクタは、他のパッケージから呼び出せないように、
064         * パッケージプライベートにしておきます。
065         * 年月データは、連続である必要があります。
066         * 途中に抜けがあるかどうかのチェックを行います。
067         *
068         * @param       data    データベース検索データ
069         * @param       isFlat  縦持ち(false)か横持ち(true)の区別
070         */
071        /* default */ CalendarDBData( final String[][] data,final boolean isFlat ) {
072                if( isFlat ) {
073                        callFlatTable( data );
074                }
075                else {
076                        callVerticalTable( data );
077                }
078        }
079
080        /**
081         * 横持ち(フラット)配列文字列のデータを元に、日付情報を構築します。
082         * 年月データは、連続である必要があります。
083         * 途中に抜けがあるかどうかのチェックを行います。
084         *
085         * @og.rev 3.8.8.0 (2007/12/22) 休日が全くない場合でも、日付設定する。
086         * @og.rev 6.4.2.0 (2016/01/29) HybsDateUtil.getDatePlus( String,int ) を直接利用するように修正します。
087         *
088         * @param data 配列文字列のデータ
089         *                     [0]:年月(YYYYMM) 200406 という形式
090         *                     [1]~[31] 1日~31日のデータ
091         *                           0:平日 / その他:休日
092         */
093        private void callFlatTable( final String[][] data ) {
094                // 8.5.4.2 (2024/01/12) PMD 7.0.0 ForLoopCanBeForeach
095//              for( int ym=0; ym<data.length; ym++ ) {
096//                      final String yyyymm = data[ym][0] ;
097                for( final String[] ym : data ) {
098                        final String yyyymm = ym[0] ;
099                        if( yyyymm.length() != 6 ) {
100                                final String errMsg = "年月(YYYYMM)は、YYYYMM(例:200406) という形式で指定して下さい。"
101                                                        + " YYYYMM [" + yyyymm + "]" ;
102                                throw new HybsSystemException( errMsg );
103                        }
104
105                        final Calendar month = HybsDateUtil.getCalendar( yyyymm + "01" );               // 当月 6.4.2.0 (2016/01/29)
106                        final int lastDay = month.getActualMaximum( Calendar.DAY_OF_MONTH );    // 月末日
107
108                        final BitSet ymData = new BitSet( lastDay+1 );
109                        for( int d=1; d<=lastDay; d++ ) {
110//                              if( ! "0".equals( data[ym][d] ) ) {
111                                if( ! "0".equals( ym[d] ) ) {
112                                        // 日付けのビットを立てます。
113                                        ymData.set( d );
114                                }
115                        }
116
117                        // 日付けのキーがあり、休日設定があれば、有効月とみなし、セットします。
118                        // 3.8.8.0 (2007/12/22) 休日が全くない場合でも、日付設定する。
119                        ymMap.put( yyyymm,ymData );
120                }
121        }
122
123        /**
124         * 縦持ち(バーティカル)配列文字列のデータを元に、日付情報を構築します。
125         * 年月データは、連続である必要があります。
126         * 途中に抜けがあるかどうかのチェックを行います。
127         *
128         * @og.rev 3.8.8.0 (2007/12/22) 休日が全くない場合でも、日付設定する。
129         * @og.rev 6.4.2.0 (2016/01/29) HybsDateUtil.getDatePlus( String,int ) を直接利用するように修正します。
130         *
131         * @param data 配列文字列のデータ
132         *                     [0]:年月(YYYYMMDD) 20040601 という形式
133         *                     [1] 0:平日 / その他:休日
134         */
135        private void callVerticalTable( final String[][] data ) {
136                String bkYyyymm = null;
137                BitSet ymData = null;
138                // 8.5.4.2 (2024/01/12) PMD 7.0.0 ForLoopCanBeForeach
139//              for( int ymd=0; ymd<data.length; ymd++ ) {
140//                      if( data[ymd][0].length() != 8 ) {
141                for( final String[] ymd : data ) {
142                        if( ymd[0].length() != 8 ) {
143                                final String errMsg = "年月日(YYYYMMDD)は、YYYYMMDD(例:20040601) という形式で指定して下さい。"
144//                                                      + " YYYYMMDD [" + data[ymd][0] + "]" ;
145                                                        + " YYYYMMDD [" + ymd[0] + "]" ;
146                                throw new HybsSystemException( errMsg );
147                        }
148
149//                      final String yyyymm = data[ymd][0].substring( 0,6 ) ;
150                        final String yyyymm = ymd[0].substring( 0,6 ) ;
151                        if( ! yyyymm.equals( bkYyyymm ) ) {     // 月のブレイク
152                                // 日付けのキーがあり、休日設定があれば、有効月とみなし、セットします。
153                                // 3.8.8.0 (2007/12/22) 休日が全くない場合でも、日付設定する。
154                                if( ymData != null && bkYyyymm != null ) {
155                                        ymMap.put( bkYyyymm,ymData );
156                                }
157                                bkYyyymm = yyyymm;
158
159                                final Calendar month = HybsDateUtil.getCalendar( yyyymm + "01" );               // 当月 6.4.2.0 (2016/01/29)
160                                final int lastDay = month.getActualMaximum( Calendar.DAY_OF_MONTH );    // 月末日
161
162                                ymData = new BitSet( lastDay+1 );
163                        }
164
165//                      if( ! "0".equals( data[ymd][1] ) ) {
166                        if( ! "0".equals( ymd[1] ) ) {
167                                // 日付けのビットを立てます。
168//                              final int dt = Integer.parseInt( data[ymd][0].substring( 6 ) );
169                                final int dt = Integer.parseInt( ymd[0].substring( 6 ) );
170                                ymData.set( dt );
171                        }
172                }
173                // 日付けのキーがあり、休日設定があれば、有効月とみなし、セットします。
174                // 3.8.8.0 (2007/12/22) 休日が全くない場合でも、日付設定する。
175                if( ymData != null && bkYyyymm != null ) {
176                        ymMap.put( bkYyyymm,ymData );
177                }
178        }
179
180        /**
181         * 指定の日付けが、休日かどうかを返します。
182         * 指定の日付けが、データベースに設定されていない場合は、すべて休日を
183         * 返すことで、存在しない事を示します。
184         *
185         * @param  day Calendar 指定の日付け
186         *
187         * @return      休日:true それ以外:false
188         *
189         */
190        @Override       // CalendarData
191        public boolean isHoliday( final Calendar day ) {
192                final String yyyymm = cal2YM( day );
193                final BitSet ymData = ymMap.get( yyyymm );
194
195                // 6.4.1.1 (2016/01/16) PMD refactoring. A method should have only one exit point, and that should be the last statement in the method
196                // 条件判定方法変更注意
197                return ymData == null || ymData.get( day.get( Calendar.DATE ) );
198        }
199
200//      /**
201//       * 指定の日付けから、範囲の間に、本日を含むかどうかを返します。
202//       * 指定の日付けが、キャッシュしているデータの最大と最小の間に
203//       * 存在しない場合は、常に false になります。
204//       * 判定は、年月日の項目のみで比較し、時分秒は無視します。
205//       *
206//       * @og.rev 3.7.1.1 (2005/05/31) 新規追加
207//       * @og.rev 3.8.8.6 (2007/04/20) today を毎回求めます。(キャッシュ対策)
208//       * @og.rev 8.5.6.1 (2024/03/29) interface のデフォルト実装を使用します。
209//       *
210//       * @param  day Calendar 指定の開始日付け
211//       * @param       scope   範囲の日数
212//       *
213//       * @return      本日:true それ以外:false
214//       */
215//      @Override       // CalendarData
216//      public boolean isContainedToday( final Calendar day,final int scope ) {
217//              final boolean rtnFlag;
218//
219//              final Calendar today = Calendar.getInstance();
220//              today.set( Calendar.HOUR_OF_DAY ,12 );  // 昼にセット
221//              today.set( Calendar.MINUTE ,0 );
222//              today.set( Calendar.SECOND ,0 );
223//
224//              if( scope == 1 ) {
225//                      // false の確率の高い方から、比較します。
226//                      rtnFlag = day.get( Calendar.DATE )  == today.get( Calendar.DATE  ) &&
227//                                              day.get( Calendar.MONTH ) == today.get( Calendar.MONTH ) &&
228//                                              day.get( Calendar.YEAR )  == today.get( Calendar.YEAR  ) ;
229//              }
230//              else {
231//                      final Calendar next = (Calendar)day.clone();
232//                      next.add( Calendar.DATE,scope );
233//                      rtnFlag = day.before( today ) && next.after( today ) ;
234//              }
235//              return rtnFlag ;
236//      }
237
238        /**
239         * 指定の開始、終了日の期間に、平日(稼働日)が何日あるか求めます。
240         * 調査月が、データベース上に存在していない場合は、エラーとします。
241         * 開始と終了が同じ日の場合は、1を返します。
242         *
243         * @param  start Calendar 開始日付け(稼働日に含めます)
244         * @param  end   Calendar 終了日付け(稼働日に含めます)
245         *
246         * @return      稼働日数
247         *
248         */
249        @Override       // CalendarData
250        public int getKadoubisu( final Calendar start,final Calendar end ) {
251
252                final String stYM = cal2YM( start );
253                final String edYM = cal2YM( end );
254
255                final SortedMap<String,BitSet> subMap = ymMap.subMap( stYM,edYM+"\0" ) ;        // 終了キーはそのままでは含まれないため。
256
257                @SuppressWarnings("rawtypes")
258//              final Map.Entry[] entrys = subMap.entrySet().toArray( new Map.Entry[subMap.size()] );
259//              final Map.Entry[] entrys = subMap.entrySet().toArray( new Map.Entry[0] );       // 8.5.4.2 (2024/01/12) PMD 7.0.0 OptimizableToArrayCall 対応
260
261                int holidaySu = 0;
262                // 8.5.4.2 (2024/01/12) PMD 7.0.0 ForLoopCanBeForeach
263//              for( int i=0; i<entrys.length; i++ ) {
264//                      final String ym = (String)entrys[i].getKey();
265//                      BitSet bt ;
266//                      if( stYM.equals( ym ) ) {
267//                              bt = ((BitSet)entrys[i].getValue()).get( start.get( Calendar.DATE ),31 );
268//                      }
269//                      else if( edYM.equals( ym ) ) {
270//                              bt = ((BitSet)entrys[i].getValue()).get( 1,end.get( Calendar.DATE )+1 );
271//                      }
272//                      else {
273//                              bt = (BitSet)entrys[i].getValue();
274//                      }
275//                      holidaySu += bt.cardinality() ;
276//              }
277                for( final Map.Entry<String,BitSet> ymEntry : subMap.entrySet() ) {
278                        final String ym = ymEntry.getKey();
279                        BitSet bt ;
280                        if( stYM.equals( ym ) ) {
281                                bt = ymEntry.getValue().get( start.get( Calendar.DATE ),31 );
282                        }
283                        else if( edYM.equals( ym ) ) {
284                                bt = ymEntry.getValue().get( 1,end.get( Calendar.DATE )+1 );
285                        }
286                        else {
287                                bt = ymEntry.getValue();
288                        }
289                        holidaySu += bt.cardinality() ;
290                }
291
292                final long diff = start.getTimeInMillis() - end.getTimeInMillis() ;
293                final int dayCount = (int)(diff/(1000*60*60*24)) + 1;   // end も含むので+1必要
294
295                return dayCount - holidaySu ;
296        }
297
298//      /**
299//       * 指定の開始日に平日のみ期間を加算して求められる日付けを返します。
300//       * これは、実稼働日計算に使用します。
301//       * 例えば、start=20040810 , span=5 で、休日がなければ、10,11,12,13,14 となり、
302//       * 20040815 を返します。
303//       * 指定の日付けや、期間加算後の日付けが、キャッシュしているデータの
304//       * 最大と最小の間に存在しない場合は、エラーとします。
305//       *
306//       * @og.rev 8.5.6.1 (2024/03/29) interface のデフォルト実装を使用します。
307//       *
308//       * @param  start Calendar   開始日付け(YYYYMMDD 形式)
309//       * @param       span    稼動期間
310//       *
311//       * @return      開始日から稼動期間を加算した日付け(当日を含む)
312//       *
313//       */
314//      @Override       // CalendarData
315//      public Calendar getAfterDay( final Calendar start,final int span ) {
316//              final Calendar tempDay = (Calendar)start.clone();
317//              int suSpan = span ;
318//              while( suSpan > 0 ) {
319//                      if( ! isHoliday( tempDay ) ) { suSpan--; }
320//                      tempDay.add(Calendar.DATE, 1);          // 日にちを進める。
321//              }
322//              return tempDay ;
323//      }
324
325        /**
326         * カレンダオブジェクトより、年月を YYYYMM 形式にした 文字列を返します。
327         *
328         * @param       cal     カレンダオブジェクト
329         * @return  カレンダのYYYYMM 文字列
330         * @og.rtnNotNull
331         */
332        private String cal2YM( final Calendar cal ) {
333                return String.valueOf(
334                                        cal.get( Calendar.YEAR ) * 100 +
335                                        cal.get( Calendar.MONTH ) + 1 );
336        }
337
338        /**
339         * オブジェクトの識別子として、詳細なカレンダ情報を返します。
340         *
341         * @return  詳細なカレンダ情報
342         * @og.rtnNotNull
343         */
344        @Override       // Object
345        public String toString() {
346                final StringBuilder rtn = new StringBuilder( BUFFER_MIDDLE )
347                        .append( "CLASS   : ").append( getClass().getName() ).append( CR );     // クラス名
348
349//              @SuppressWarnings("rawtypes")
350//              final Map.Entry[] entrys = ymMap.entrySet().toArray( new Map.Entry[ymMap.size()] );
351//              final Map.Entry[] entrys = ymMap.entrySet().toArray( new Map.Entry[0] );        // 8.5.4.2 (2024/01/12) PMD 7.0.0 OptimizableToArrayCall 対応
352
353                // 8.5.4.2 (2024/01/12) PMD 7.0.0 ForLoopCanBeForeach
354//              for( int i=0; i<entrys.length; i++ ) {
355//                      final String yyyymm = (String)entrys[i].getKey();
356                for( final Map.Entry<String,BitSet> ymEntry : ymMap.entrySet() ) {
357                        final String yyyymm = ymEntry.getKey();
358
359                        rtn.append( yyyymm )
360                                .append( ':' )                                          // 6.0.2.5 (2014/10/31) char を append する。
361//                              .append( (BitSet)entrys[i].getValue() )
362                                .append( ymEntry.getValue() )
363                                .append( CR );
364                }
365
366                return rtn.toString();
367        }
368}