001package org.cpsolver.coursett.model;
002
003import java.util.BitSet;
004import java.util.Enumeration;
005
006import org.cpsolver.coursett.Constants;
007import org.cpsolver.ifs.util.ToolBox;
008
009
010/**
011 * Time part of placement.
012 * 
013 * @version CourseTT 1.3 (University Course Timetabling)<br>
014 *          Copyright (C) 2006 - 2014 Tomáš Müller<br>
015 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
016 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
017 * <br>
018 *          This library is free software; you can redistribute it and/or modify
019 *          it under the terms of the GNU Lesser General Public License as
020 *          published by the Free Software Foundation; either version 3 of the
021 *          License, or (at your option) any later version. <br>
022 * <br>
023 *          This library is distributed in the hope that it will be useful, but
024 *          WITHOUT ANY WARRANTY; without even the implied warranty of
025 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
026 *          Lesser General Public License for more details. <br>
027 * <br>
028 *          You should have received a copy of the GNU Lesser General Public
029 *          License along with this library; if not see
030 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
031 */
032
033public class TimeLocation {
034    private int iStartSlot;
035
036    private int iPreference;
037    private double iNormalizedPreference;
038
039    private Long iTimePatternId = null;
040    private int iHashCode;
041
042    private int iDayCode;
043    private int iLength;
044    private int iNrMeetings;
045    private int iBreakTime;
046
047    private BitSet iWeekCode;
048    private Long iDatePatternId = null;
049    private String iDatePatternName = null;
050    private int iDatePreference;
051
052    /**
053     * Constructor
054     * 
055     * @param dayCode
056     *            days (combination of 1 for Monday, 2 for Tuesday, ...)
057     * @param startTime
058     *            start slot
059     * @param length
060     *            number of slots
061     * @param pref
062     *            time preference
063     * @param normPref normalized preference
064     * @param datePatternPreference date pattern preference
065     * @param datePatternId date pattern unique id
066     * @param datePatternName date pattern name
067     * @param weekCode date pattern (binary string with 1 for each day when classes take place)
068     * @param breakTime break time in minutes
069     */
070    public TimeLocation(int dayCode, int startTime, int length, int pref, double normPref, int datePatternPreference,
071            Long datePatternId, String datePatternName, BitSet weekCode, int breakTime) {
072        iPreference = pref;
073        iNormalizedPreference = normPref;
074        iStartSlot = startTime;
075        iDayCode = dayCode;
076        iLength = length;
077        iBreakTime = breakTime;
078        iNrMeetings = 0;
079        for (int i = 0; i < Constants.DAY_CODES.length; i++) {
080            if ((iDayCode & Constants.DAY_CODES[i]) == 0)
081                continue;
082            iNrMeetings++;
083        }
084        iHashCode = combine(combine(iDayCode, iStartSlot), iLength);
085        iDatePatternName = datePatternName;
086        iWeekCode = weekCode;
087        iDatePatternId = datePatternId;
088        if (iDatePatternName == null)
089            iDatePatternName = "not set";
090        iDatePreference = datePatternPreference;
091        if (iWeekCode == null) {
092            iWeekCode = new BitSet(366);
093            for (int i = 0; i <= 365; i++)
094                iWeekCode.set(i);
095        }
096    }
097    
098    public TimeLocation(int dayCode, int startTime, int length, int pref, double normPref, Long datePatternId,
099            String datePatternName, BitSet weekCode, int breakTime) {
100        this(dayCode, startTime, length, pref, normPref, 0, datePatternId, datePatternName, weekCode, breakTime);
101    }
102
103    /** Number of meetings 
104     * @return number of meetings
105     **/
106    public int getNrMeetings() {
107        return iNrMeetings;
108    }
109
110    public int getBreakTime() {
111        return iBreakTime;
112    }
113
114    public void setBreakTime(int breakTime) {
115        iBreakTime = breakTime;
116    }
117
118    private static int combine(int a, int b) {
119        int ret = 0;
120        for (int i = 0; i < 15; i++)
121            ret = ret | ((a & (1 << i)) << i) | ((b & (1 << i)) << (i + 1));
122        return ret;
123    }
124
125    /** Days (combination of 1 for Monday, 2 for Tuesday, ...) 
126     * @return days of the week of this time
127     **/
128    public int getDayCode() {
129        return iDayCode;
130    }
131
132    /** Days for printing purposes 
133     * @return day header (e.g., MWF for Monday - Wednesday - Friday time)
134     **/
135    public String getDayHeader() {
136        StringBuffer sb = new StringBuffer();
137        for (int i = 0; i < Constants.DAY_CODES.length; i++)
138            if ((iDayCode & Constants.DAY_CODES[i]) != 0)
139                sb.append(Constants.DAY_NAMES_SHORT[i]);
140        return sb.toString();
141    }
142
143    /** Start time for printing purposes
144     * @param useAmPm use 12-hour format 
145     * @return time header (e.g., 7:30a)
146     **/
147    public String getStartTimeHeader(boolean useAmPm) {
148        int min = iStartSlot * Constants.SLOT_LENGTH_MIN + Constants.FIRST_SLOT_TIME_MIN;
149        int h = min / 60;
150        int m = min % 60;
151        if (useAmPm)
152            return (h > 12 ? h - 12 : h) + ":" + (m < 10 ? "0" : "") + m + (h >= 12 ? "p" : "a");
153        else
154            return h + ":" + (m < 10 ? "0" : "") + m;
155    }
156    
157    /** Start time for printing purposes 
158     * @return time header (e.g., 7:30a)
159     **/
160    @Deprecated
161    public String getStartTimeHeader() {
162        return getStartTimeHeader(true);
163    }
164
165    /** End time for printing purposes 
166     * @param useAmPm use 12-hour format
167     * @return end time (e.g., 8:20a)
168     **/
169    public String getEndTimeHeader(boolean useAmPm) {
170        int min = (iStartSlot + iLength) * Constants.SLOT_LENGTH_MIN + Constants.FIRST_SLOT_TIME_MIN - getBreakTime();
171        int m = min % 60;
172        int h = min / 60;
173        if (useAmPm)
174            return (h > 12 ? h - 12 : h) + ":" + (m < 10 ? "0" : "") + m + (h >= 12 ? "p" : "a");
175        else
176            return h + ":" + (m < 10 ? "0" : "") + m;
177    }
178    
179    /** End time for printing purposes 
180     * @return end time (e.g., 8:20a)
181     **/
182    @Deprecated
183    public String getEndTimeHeader() {
184        return getEndTimeHeader(true);
185    }
186
187
188    /** End time for printing purposes 
189     * @param useAmPm use 12-hour format
190     * @return end time not counting break time (e.g., 8:30a)
191     **/
192    public String getEndTimeHeaderNoAdj(boolean useAmPm) {
193        int min = (iStartSlot + iLength) * Constants.SLOT_LENGTH_MIN + Constants.FIRST_SLOT_TIME_MIN;
194        int m = min % 60;
195        int h = min / 60;
196        if (useAmPm)
197            return (h > 12 ? h - 12 : h) + ":" + (m < 10 ? "0" : "") + m + (h >= 12 ? "p" : "a");
198        else
199            return h + ":" + (m < 10 ? "0" : "") + m;
200    }
201
202    /** End time for printing purposes 
203     * @return end time not counting break time (e.g., 8:30a)
204     **/
205    @Deprecated
206    public String getEndTimeHeaderNoAdj() {
207        return getEndTimeHeaderNoAdj(true);
208    }
209    
210    /** Start slot
211     * @return start slot
212     **/
213    public int getStartSlot() {
214        return iStartSlot;
215    }
216
217    /** Used slots in a day (combination of 1..first, 2..second,...) */
218
219    /** true if days overlap
220     * @param anotherLocation another time
221     * @return true if days of the week overlaps
222     **/
223    public boolean shareDays(TimeLocation anotherLocation) {
224        return ((iDayCode & anotherLocation.iDayCode) != 0);
225    }
226
227    /** number of overlapping days 
228     * @param anotherLocation another time
229     * @return number of days of the week that the two times share
230     **/
231    public int nrSharedDays(TimeLocation anotherLocation) {
232        int ret = 0;
233        for (int i = 0; i < Constants.NR_DAYS; i++) {
234            if ((iDayCode & Constants.DAY_CODES[i]) == 0)
235                continue;
236            if ((anotherLocation.iDayCode & Constants.DAY_CODES[i]) == 0)
237                continue;
238            ret++;
239        }
240        return ret;
241    }
242
243    /** true if hours overlap 
244     * @param anotherLocation another time
245     * @return true if the two times overlap in time (just the time of the day is considered)
246     **/
247    public boolean shareHours(TimeLocation anotherLocation) {
248        return (iStartSlot + iLength > anotherLocation.iStartSlot)
249                && (anotherLocation.iStartSlot + anotherLocation.iLength > iStartSlot);
250    }
251
252    /** number of overlapping time slots (ignoring days) 
253     * @param anotherLocation another time
254     * @return number of time slots the two location overlap
255     **/
256    public int nrSharedHours(TimeLocation anotherLocation) {
257        int end = Math.min(iStartSlot + iLength, anotherLocation.iStartSlot + anotherLocation.iLength);
258        int start = Math.max(iStartSlot, anotherLocation.iStartSlot);
259        return (end < start ? 0 : end - start);
260    }
261
262    /** true if weeks overlap
263     * @param anotherLocation another time
264     * @return true if the date patterns overlap
265     */
266    public boolean shareWeeks(TimeLocation anotherLocation) {
267        return iWeekCode.intersects(anotherLocation.iWeekCode);
268    }
269
270    /** true if weeks overlap
271     * @param weekCode another date pattern
272     * @return true if the date patterns overlap
273     */
274    public boolean shareWeeks(BitSet weekCode) {
275        return iWeekCode.intersects(weekCode);
276    }
277
278    public boolean hasDay(int day) {
279        return iWeekCode.get(day);
280    }
281
282    /** true if overlap 
283     * @param anotherLocation another time
284     * @return true if the two times overlap, this means that all three checks {@link TimeLocation#shareDays(TimeLocation)}, {@link TimeLocation#shareHours(TimeLocation)} and {@link TimeLocation#shareWeeks(TimeLocation)} are true.
285     **/
286    public boolean hasIntersection(TimeLocation anotherLocation) {
287        return shareDays(anotherLocation) && shareHours(anotherLocation) && shareWeeks(anotherLocation);
288    }
289
290    /** Used slots 
291     * @return enumeration of used slots
292     **/
293    public IntEnumeration getSlots() {
294        return new SlotsEnum();
295    }
296
297    /** Used start slots (for each meeting) 
298     * @return enumeration of start slots for each meeting of the time
299     **/
300    public IntEnumeration getStartSlots() {
301        return new StartSlotsEnum();
302    }
303
304    /** Days 
305     * @return enumeration of days of week of the time
306     **/
307    public IntEnumeration getDays() {
308        return new DaysEnum();
309    }
310
311    private int[] iDaysCache = null;
312    public int[] getDaysArray() {
313        if (iDaysCache == null) {
314            iDaysCache = new int[getNrMeetings()];
315            int i = 0;
316            for (Enumeration<Integer> e = getDays(); e.hasMoreElements();)
317                iDaysCache[i++] = e.nextElement();
318        }
319        return iDaysCache;
320    }
321
322    /** Text representation 
323     * @param useAmPm 12-hour format
324     * @return time name (e.g., MWF 7:30a)
325     **/
326    public String getName(boolean useAmPm) {
327        return getDayHeader() + " " + getStartTimeHeader(useAmPm);
328    }
329    
330    @Deprecated
331    public String getName() {
332        return getName(true);
333    }
334
335    public String getLongName(boolean useAmPm) {
336        return getDayHeader() + " " + getStartTimeHeader(useAmPm) + " - " + getEndTimeHeader(useAmPm) + " " + getDatePatternName();
337    }
338    
339    @Deprecated
340    public String getLongName() {
341        return getLongName(true);
342    }
343
344    public String getLongNameNoAdj(boolean useAmPm) {
345        return getDayHeader() + " " + getStartTimeHeader(useAmPm) + " - " + getEndTimeHeaderNoAdj(useAmPm) + " " + getDatePatternName();
346    }
347    
348    public String getLongNameNoAdj() {
349        return getLongNameNoAdj(true);
350    }
351
352    /** Preference 
353     * @return time preference
354     **/
355    public int getPreference() {
356        return iPreference;
357    }
358
359    public void setPreference(int preference) {
360        iPreference = preference;
361    }
362
363    /** Length 
364     * @return time length (in the number of slots)
365     **/
366    public int getLength() {
367        return iLength;
368    }
369
370    /** Length 
371     * @return time length (in the number of slots)
372     **/
373    public int getNrSlotsPerMeeting() {
374        return iLength;
375    }
376
377    /** Normalized preference 
378     * @return normalized preference
379     **/
380    public double getNormalizedPreference() {
381        return iNormalizedPreference;
382    }
383
384    public void setNormalizedPreference(double normalizedPreference) {
385        iNormalizedPreference = normalizedPreference;
386    }
387
388    /** Time pattern model (can be null) 
389     * @return time pattern unique id
390     **/
391    public Long getTimePatternId() {
392        return iTimePatternId;
393    }
394
395    public Long getDatePatternId() {
396        return iDatePatternId;
397    }
398
399    public void setTimePatternId(Long timePatternId) {
400        iTimePatternId = timePatternId;
401    }
402
403    public BitSet getWeekCode() {
404        return iWeekCode;
405    }
406
407    public String getDatePatternName() {
408        return iDatePatternName;
409    }
410
411    public void setDatePattern(Long datePatternId, String datePatternName, BitSet weekCode) {
412        iDatePatternId = datePatternId;
413        iDatePatternName = datePatternName;
414        iWeekCode = weekCode;
415    }
416    
417    public int getDatePatternPreference() {
418        return iDatePreference;
419    }
420
421    @Override
422    public String toString() {
423        return getName() + " (" + iNormalizedPreference + ")";
424    }
425
426    @Override
427    public int hashCode() {
428        return iHashCode;
429    }
430
431    private class StartSlotsEnum implements IntEnumeration {
432        int day = -1;
433        boolean hasNext = false;
434
435        private StartSlotsEnum() {
436            hasNext = nextDay();
437        }
438
439        boolean nextDay() {
440            do {
441                day++;
442                if (day >= Constants.DAY_CODES.length)
443                    return false;
444            } while ((Constants.DAY_CODES[day] & iDayCode) == 0);
445            return true;
446        }
447
448        @Override
449        public boolean hasMoreElements() {
450            return hasNext;
451        }
452
453        @Override
454        public Integer nextElement() {
455            int slot = (day * Constants.SLOTS_PER_DAY) + iStartSlot;
456            hasNext = nextDay();
457            return slot;
458        }
459        
460        @Deprecated
461        @Override
462        public Integer nextInt() {
463            return nextElement();
464        }
465    }
466
467    private class DaysEnum extends StartSlotsEnum {
468        private DaysEnum() {
469            super();
470        }
471
472        @Override
473        public Integer nextElement() {
474            int ret = day;
475            hasNext = nextDay();
476            return ret;
477        }
478    }
479
480    private class SlotsEnum extends StartSlotsEnum {
481        int pos = 0;
482
483        private SlotsEnum() {
484            super();
485        }
486
487        private boolean nextSlot() {
488            if (pos + 1 < iLength) {
489                pos++;
490                return true;
491            }
492            if (nextDay()) {
493                pos = 0;
494                return true;
495            }
496            return false;
497        }
498
499        @Override
500        public Integer nextElement() {
501            int slot = (day * Constants.SLOTS_PER_DAY) + iStartSlot + pos;
502            hasNext = nextSlot();
503            return slot;
504        }
505    }
506
507    @Override
508    public boolean equals(Object o) {
509        if (o == null || !(o instanceof TimeLocation))
510            return false;
511        TimeLocation t = (TimeLocation) o;
512        if (getStartSlot() != t.getStartSlot())
513            return false;
514        if (getLength() != t.getLength())
515            return false;
516        if (getDayCode() != t.getDayCode())
517            return false;
518        return ToolBox.equals(getTimePatternId(), t.getTimePatternId())
519                && ToolBox.equals(getDatePatternId(), t.getDatePatternId());
520    }
521
522    public int getNrWeeks() {
523        return getNrWeeks(0, iWeekCode.size() - 1);
524    }
525
526    public int getNrWeeks(int startDay, int endDay) {
527        /*
528         * BitSet x = new BitSet(1+(endDay-startDay)/Constants.NR_DAYS); for
529         * (int i=iWeekCode.nextSetBit(startDay); i<=endDay && i>=0;
530         * i=iWeekCode.nextSetBit(i+1)) x.set((i-startDay)/Constants.NR_DAYS);
531         * return x.cardinality();
532         */
533        int card = iWeekCode.get(startDay, endDay).cardinality();
534        if (card == 0)
535            return 0;
536        if (card <= 7)
537            return 1;
538        return (5 + card) / 6;
539    }
540    
541    public interface IntEnumeration extends Enumeration<Integer> {
542        @Deprecated
543        public Integer nextInt();
544    }
545    
546    private Integer iFirstMeeting = null;
547    public int getFirstMeeting(int dayOfWeekOffset) {
548        if (iFirstMeeting == null) {
549            int idx = -1;
550            while ((idx = getWeekCode().nextSetBit(1 + idx)) >= 0) {
551                int dow = (idx + dayOfWeekOffset) % 7;
552                if ((getDayCode() & Constants.DAY_CODES[dow]) != 0) break;
553            }
554            iFirstMeeting = idx;
555        }
556        return iFirstMeeting;
557    }
558    
559    /** List dates when this time location meets. 
560     * @return enumeration of dates of this time (indexes to the {@link TimeLocation#getWeekCode()} for matching days of the week)
561     **/
562    public IntEnumeration getDates(int dayOfWeekOffset) {
563        return new DateEnum(dayOfWeekOffset);
564    }
565    
566    /**
567     * Check if the given time location has a particular date
568     * @param date a date, expressed as an index to the {@link TimeLocation#getWeekCode()} 
569     * @param dayOfWeekOffset day of the week offset for the weeks pattern
570     * @return true if this time location is meeting on the given date
571     */
572    public boolean hasDate(int date, int dayOfWeekOffset) {
573        if (getWeekCode().get(date)) {
574            int dow = (date + dayOfWeekOffset) % 7;
575            if ((getDayCode() & Constants.DAY_CODES[dow]) != 0) return true;
576        }
577        return false;
578    }
579    
580    /**
581     * Count how many times this time location is meeting
582     * @param dayOfWeekOffset day of the week offset for the weeks pattern
583     * @return number of dates during which this time location is meeting
584     */
585    public int countDates(int dayOfWeekOffset) {
586        int idx = -1;
587        int count = 0;
588        while ((idx = getWeekCode().nextSetBit(1 + idx)) >= 0) {
589            int dow = (idx + dayOfWeekOffset) % 7;
590            if ((getDayCode() & Constants.DAY_CODES[dow]) != 0) count++;
591        }
592        return count;
593    }
594    
595    private class DateEnum implements IntEnumeration {
596        int dayOfWeekOffset = 0;
597        int nextDate = -1;
598        boolean hasNext = false;
599
600        private DateEnum(int dayOfWeekOffset) {
601            this.dayOfWeekOffset = dayOfWeekOffset;
602            hasNext = nextDate();
603        }
604
605        boolean nextDate() {
606            while (true) {
607                nextDate = getWeekCode().nextSetBit(1 + nextDate);
608                if (nextDate < 0) return false;
609                int dow = (nextDate + dayOfWeekOffset) % 7;
610                if ((getDayCode() & Constants.DAY_CODES[dow]) != 0) return true;
611            }
612        }
613
614        @Override
615        public boolean hasMoreElements() {
616            return hasNext;
617        }
618
619        @Override
620        public Integer nextElement() {
621            int ret = nextDate;
622            hasNext = nextDate();
623            return ret;
624        }
625        
626        @Deprecated
627        @Override
628        public Integer nextInt() {
629            return nextElement();
630        }
631    }
632}