Coverage Report - org.openpermis.basic.PartialTime
 
Classes in this File Line Coverage Branch Coverage Complexity
PartialTime
68%
120/176
58%
107/184
6.524
 
 1  
 /*
 2  
  * Copyright (c) 2009, Swiss Federal Department of Defence Civil Protection and Sport
 3  
  *                     (http://www.vbs.admin.ch)
 4  
  * Copyright (c) 2009, Ergon Informatik AG (http://www.ergon.ch)
 5  
  * All rights reserved.
 6  
  *
 7  
  * Licensed under the Open Permis License which accompanies this distribution,
 8  
  * and is available at http://www.openpermis.org/BSDlicenceKent.txt
 9  
  */
 10  
 
 11  
 package org.openpermis.basic;
 12  
 
 13  
 import java.util.Arrays;
 14  
 import java.util.Date;
 15  
 import java.util.regex.Matcher;
 16  
 import java.util.regex.Pattern;
 17  
 
 18  
 import org.joda.time.DateTime;
 19  
 import org.joda.time.DateTimeZone;
 20  
 
 21  
 import org.openpermis.policy.TimeStamp;
 22  
 import org.openpermis.policy.io.xml.TimeUtility;
 23  
 
 24  
 
 25  
 /**
 26  
  * A {@link PartialTime} may represent a specific instant in time or a not yet fully specified
 27  
  * instance in time. That means every part (year, days, time zone ...) is optional.
 28  
  * <p>
 29  
  * Comparison of two {@link PartialTime}'s are only supported if at least one instance is fully
 30  
  * specified.<br/>
 31  
  * Comparison simply ignores positions with an asterix.<br/>
 32  
  * A comparison is always relative to a given {@link TimeStamp}. This is due the fact that an
 33  
  * {@link PartialTime} may be specified in local time.
 34  
  * <p>
 35  
  * Time: <code>(*|YYYY)-(*|MM)-(*|DD)T(*|hh):(*|mm):(*|ss)</code>.
 36  
  * Zone: <code>Z|([+-]hh:ss)</code>.
 37  
  * <p>
 38  
  * Examples:<br/>
 39  
  * <ul>
 40  
  *         <li>2008-02-11T14:15:00Z</li>
 41  
  *         <li>2008-02-11T14:15:00+02:00</li>
 42  
  *        <li>*-02-11T14:15:00</li>
 43  
  * </ul>
 44  
  */
 45  
 public final class PartialTime {
 46  
 
 47  
         //---- State
 48  
 
 49  
         private int year;
 50  
 
 51  
         private boolean isYearAsterix;
 52  
 
 53  
         private int monthOfYear;
 54  
 
 55  
         private int dayOfMonth;
 56  
 
 57  
         private int hourOfDay;
 58  
 
 59  
         private int minuteOfHour;
 60  
 
 61  
         private int secondOfMinute;
 62  
 
 63  228
         private int[] zone = new int[2];
 64  
 
 65  
         boolean hasZone;
 66  
 
 67  
         private DateTimeZone defaultZone;
 68  
 
 69  
 
 70  
         //---- Constructors
 71  
 
 72  
         /**
 73  
          * Creates an {@link PartialTime} from a {@link Date}.
 74  
          * @param date a {@link Date}.
 75  
          */
 76  77
         public PartialTime (Date date) {
 77  77
                 final DateTime dateTime = new DateTime(date, DateTimeZone.UTC);
 78  
 
 79  77
                 this.isYearAsterix = false;
 80  77
                 this.year = dateTime.getYear();
 81  77
                 this.monthOfYear = dateTime.getMonthOfYear();
 82  77
                 this.dayOfMonth = dateTime.getDayOfMonth();
 83  77
                 this.hourOfDay = dateTime.getHourOfDay();
 84  77
                 this.minuteOfHour = dateTime.getMinuteOfHour();
 85  77
                 this.secondOfMinute = dateTime.getSecondOfMinute();
 86  
 
 87  77
                 this.hasZone = true;
 88  77
                 this.zone[0] = 0;
 89  77
                 this.zone[1] = 0;
 90  77
         }
 91  
 
 92  
         /**
 93  
          * Creates a new {@link PartialTime} from a string with a default time zone. The default zone
 94  
          * may be null and therefore the local time zone at evaluation time is taken.
 95  
          * @param string see {@link PartialTime} class description.
 96  
          * @param defaultZone a default {@link DateTimeZone}.
 97  
          * @since 0.3.0
 98  
          */
 99  
         public PartialTime (String string, DateTimeZone defaultZone, boolean allowWildcards)
 100  
                 throws NumberFormatException
 101  151
         {
 102  151
                 this.defaultZone = defaultZone;
 103  151
                 this.hasZone = false;
 104  151
                 parse(string);
 105  151
                 if (!allowWildcards && !isComplete()) {
 106  0
                         throw new NumberFormatException("No wildcards allowd.");
 107  
                 }
 108  151
                 checkTimeAndOffset();
 109  150
         }
 110  
 
 111  
         /**
 112  
          * Creates a new {@link PartialTime} with a default time zone. The default zone
 113  
          * may be null and therefore the local time zone at evaluation time is used.
 114  
          * 
 115  
          * @param year the year.
 116  
          * @param monthOfYear the month of year.
 117  
          * @param dayOfMonth the day of month.
 118  
          * @param hourOfDay the hour of date.
 119  
          * @param minuteOfHour the minute of hour.
 120  
          * @param secondOfMinute the second of minute.
 121  
          * @param defaultZone a default {@link DateTimeZone}.
 122  
          * @since 0.3.0
 123  
          */
 124  
         public PartialTime (
 125  
                 boolean isYearAsterix,
 126  
                 int year,
 127  
                 int monthOfYear,
 128  
                 int dayOfMonth,
 129  
                 int hourOfDay,
 130  
                 int minuteOfHour,
 131  
                 int secondOfMinute,
 132  
                 boolean hasZone,
 133  
                 int offsetHours,
 134  
                 int offsetMinutes,
 135  
                 DateTimeZone defaultZone,
 136  
                 boolean allowWildcards
 137  0
         ) {
 138  0
                 this.isYearAsterix = isYearAsterix;
 139  0
                 this.year = year;
 140  0
                 this.monthOfYear = monthOfYear;
 141  0
                 this.dayOfMonth = dayOfMonth;
 142  0
                 this.hourOfDay = hourOfDay;
 143  0
                 this.minuteOfHour = minuteOfHour;
 144  0
                 this.secondOfMinute = secondOfMinute;
 145  0
                 this.hasZone = hasZone;
 146  0
                 this.zone[0] = offsetHours;
 147  0
                 this.zone[1] = offsetMinutes;
 148  0
                 this.defaultZone = defaultZone;
 149  0
                 if (!allowWildcards && !isComplete()) {
 150  0
                         throw new NumberFormatException("No wildcards allowd.");
 151  
                 }
 152  0
                 checkTimeAndOffset();
 153  0
         }
 154  
         
 155  
         //---- Methods
 156  
 
 157  
         /**
 158  
          * Returns true if this time is complete defined and contains no asterix's.
 159  
          * @return true if this time is complete defined and contains no asterix's.
 160  
          * @since 0.3.0
 161  
          */
 162  
         public boolean isComplete () {
 163  504
                 if (this.isYearAsterix ||
 164  
                         this.monthOfYear == -1 ||
 165  
                         this.dayOfMonth == -1 ||
 166  
                         this.hourOfDay == -1 ||
 167  
                         this.minuteOfHour == -1 ||
 168  
                         this.secondOfMinute == -1
 169  
                 ) {
 170  4
                         return false;
 171  
                 }
 172  500
                 return true;
 173  
         }
 174  
 
 175  
         /**
 176  
          * Returns true if this time is defined in local time zone.
 177  
          * @return true if this time is defined in local time zone.
 178  
          * @since 0.3.0
 179  
          */
 180  
         private boolean inLocalZone () {
 181  159
                 return !this.hasZone && this.zone == null;
 182  
         }
 183  
 
 184  
         /**
 185  
          * Returns true if this time is in the same time zone as other.
 186  
          * @return true if this time is in the same time zone as other.
 187  
          * @since 0.3.0
 188  
          */
 189  
         public boolean inSameTimeZone (PartialTime other) {
 190  53
                 if (inLocalZone() && other.inLocalZone()) {
 191  0
                         return true;
 192  53
                 } else if (inLocalZone() || other.inLocalZone()) {
 193  0
                         return false;
 194  
                 }
 195  
 
 196  
                 final DateTimeZone t;
 197  
                 final DateTimeZone o;
 198  53
                 if (this.hasZone) {
 199  53
                         t = DateTimeZone.forOffsetHoursMinutes(this.zone[0], this.zone[1]);
 200  
                 } else {
 201  0
                         t = this.defaultZone;
 202  
                 }
 203  53
                 if (other.hasZone) {
 204  53
                         o = DateTimeZone.forOffsetHoursMinutes(other.zone[0], other.zone[1]);
 205  
                 } else {
 206  0
                         o = other.defaultZone;
 207  
                 }
 208  53
                 return t.equals(o);
 209  
         }
 210  
 
 211  
         /**
 212  
          * Returns this as {@link DateTime}.
 213  
          * @param timeStamp a {@link TimeStamp}.
 214  
          * @return a {@link DateTime}.
 215  
          * @since 0.3.0
 216  
          */
 217  
         public DateTime toDateTime (TimeStamp timeStamp) {
 218  183
                 if (!isComplete()) {
 219  0
                         throw new IllegalStateException("Not complete.");
 220  
                 }
 221  183
                 return new DateTime(
 222  
                         this.year,
 223  
                         this.monthOfYear,
 224  
                         this.dayOfMonth,
 225  
                         this.hourOfDay,
 226  
                         this.minuteOfHour,
 227  
                         this.secondOfMinute,
 228  
                         0,
 229  
                         toDateTimeZone(timeStamp)
 230  
                 );
 231  
         }
 232  
 
 233  
         /**
 234  
          * Returns true if this and other are comparable. Two times are not comparable if they both
 235  
          * contains asterix's.
 236  
          * @param other an {@link PartialTime}.
 237  
          * @return true if this and other are comparable.
 238  
          * @since 0.3.0
 239  
          */
 240  
         public boolean isComparableWith (PartialTime other) {
 241  63
                 return isComplete() || other.isComplete();
 242  
         }
 243  
 
 244  
         /**
 245  
          * Return true if this time is after other time.
 246  
          * @param other an {@link PartialTime}.
 247  
          * @param timeStamp evaluation time.
 248  
          * @since 0.3.0
 249  
          */
 250  
         public boolean isAfter (PartialTime other, TimeStamp timeStamp) {
 251  5
                 return compareTo(other, timeStamp) > 0;
 252  
         }
 253  
 
 254  
         /**
 255  
          * Return true if this time is before other time.
 256  
          * @param other an {@link PartialTime}.
 257  
          * @param timeStamp evaluation time.
 258  
          * @since 0.3.0
 259  
          */
 260  
         public boolean isBefore (PartialTime other, TimeStamp timeStamp) {
 261  55
                 return compareTo(other, timeStamp) < 0;
 262  
         }
 263  
 
 264  
         /**
 265  
          * Return true if this time is equal other time.
 266  
          * @param other an {@link PartialTime}.
 267  
          * @param timeStamp evaluation time.
 268  
          * @since 0.3.0
 269  
          */
 270  
         public boolean isEqual (PartialTime other, TimeStamp timeStamp) {
 271  2
                 return compareTo(other, timeStamp) == 0;
 272  
         }
 273  
 
 274  
         /**
 275  
          * @since 0.3.0
 276  
          */
 277  
         private int compareTo (PartialTime other, TimeStamp timeStamp) {
 278  
                 // Both are incomplete.
 279  62
                 if (!isComparableWith(other)) {
 280  1
                         throw new IllegalStateException("Not comparable.");
 281  
                 }
 282  
 
 283  
                 // Both are complete.
 284  61
                 if (isComplete() && other.isComplete()) {
 285  61
                         return toDateTime(timeStamp).compareTo(other.toDateTime(timeStamp));
 286  
                 }
 287  
 
 288  
                 // Only one is complete.
 289  
                 DateTime complete;
 290  
                 PartialTime incomplete;
 291  
                 int sign;
 292  0
                 if (isComplete()) {
 293  0
                         complete = toDateTime(timeStamp).toDateTime(other.toDateTimeZone(timeStamp));
 294  0
                         incomplete = other;
 295  0
                         sign = 1;
 296  
                 } else {
 297  0
                         complete = other.toDateTime(timeStamp).toDateTime(toDateTimeZone(timeStamp));
 298  0
                         incomplete = this;
 299  0
                         sign = -1;
 300  
                 }
 301  
 
 302  0
                 if (!incomplete.isYearAsterix) {
 303  0
                         return sign * (incomplete.year - complete.getYear());
 304  
                 }
 305  0
                 if (incomplete.monthOfYear != -1) {
 306  0
                         return sign * (incomplete.monthOfYear - complete.getMonthOfYear());
 307  
                 }
 308  0
                 if (incomplete.dayOfMonth != -1) {
 309  0
                         return sign * (incomplete.dayOfMonth - complete.getDayOfMonth());
 310  
                 }
 311  0
                 if (incomplete.hourOfDay != -1) {
 312  0
                         return sign * (incomplete.hourOfDay - complete.getHourOfDay());
 313  
                 }
 314  0
                 if (incomplete.minuteOfHour != -1) {
 315  0
                         return sign * (incomplete.minuteOfHour - complete.getMinuteOfHour());
 316  
                 }
 317  0
                 if (incomplete.secondOfMinute != -1) {
 318  0
                         return sign * (incomplete.secondOfMinute - complete.getSecondOfMinute());
 319  
                 }
 320  
                 // Equal.
 321  0
                 return 0;
 322  
         }
 323  
 
 324  
         /**
 325  
          * @since 0.3.0
 326  
          */
 327  
         private void parse (String string) {
 328  
 
 329  151
                 final String four = "(\\d\\d\\d\\d|\\*)";
 330  151
                 final String two = "(\\d\\d|\\*)";
 331  151
                 final String time = four + "-" + two + "-" + two + "T" + two + ":" + two + ":" + two;
 332  151
                 final String timeZone = "(Z)|([+-])" + "(\\d\\d)" + ":" + "(\\d\\d)";
 333  151
                 final String timeAndZone = time + "(" + timeZone + ")?";
 334  
 
 335  151
                 final int gYear = 1;
 336  151
                 final int gMonthOfYear = 2;
 337  151
                 final int gDayOfMonth = 3;
 338  151
                 final int gHourOfDay = 4;
 339  151
                 final int gMinuteOfHour = 5;
 340  151
                 final int gSecondOfMinute = 6;
 341  151
                 final int gZoneComplete = 7;
 342  151
                 final int gZoneZ = 8;
 343  151
                 final int gZoneSign = 9;
 344  151
                 final int gZoneHours = 10;
 345  151
                 final int gZoneMinuteOfHour = 11;
 346  
 
 347  151
                 final Pattern pattern = Pattern.compile(timeAndZone);
 348  151
                 final Matcher matcher = pattern.matcher(string);
 349  
 
 350  151
                 if (matcher.matches()) {
 351  151
                         if (matcher.group(gZoneComplete) != null) {
 352  40
                                 this.hasZone = true;
 353  40
                                 this.zone = parseZone(
 354  
                                         matcher.group(gZoneZ),
 355  
                                         matcher.group(gZoneSign),
 356  
                                         matcher.group(gZoneHours),
 357  
                                         matcher.group(gZoneMinuteOfHour)
 358  
                                 );
 359  
                         }
 360  
 
 361  151
                         parseDateAndTime(
 362  
                                 matcher.group(gYear),
 363  
                                 matcher.group(gMonthOfYear),
 364  
                                 matcher.group(gDayOfMonth),
 365  
                                 matcher.group(gHourOfDay),
 366  
                                 matcher.group(gMinuteOfHour),
 367  
                                 matcher.group(gSecondOfMinute)
 368  
                         );
 369  
 
 370  
                 } else {
 371  0
                         throw new NumberFormatException();
 372  
                 }
 373  151
         }
 374  
 
 375  
         /**
 376  
          * @since 0.3.0
 377  
          */
 378  
         private void parseDateAndTime (String y, String mo, String d, String h, String mi, String s) {
 379  151
                 if ("*".equals(y)) {
 380  0
                         this.isYearAsterix = true;
 381  
                 } else {
 382  151
                         this.isYearAsterix = false;
 383  
                 }
 384  151
                 this.year = "*".equals(y) ? -1 : Integer.parseInt(y);
 385  151
                 this.monthOfYear = "*".equals(mo) ? -1 : Integer.parseInt(mo);
 386  151
                 this.dayOfMonth = "*".equals(d) ? -1 : Integer.parseInt(d);
 387  151
                 this.hourOfDay = "*".equals(h) ? -1 : Integer.parseInt(h);
 388  151
                 this.minuteOfHour = "*".equals(mi) ? -1 : Integer.parseInt(mi);
 389  151
                 this.secondOfMinute = "*".equals(s) ? -1 : Integer.parseInt(s);
 390  151
         }
 391  
 
 392  
         /**
 393  
          * @since 0.3.0
 394  
          */
 395  
         private void checkTimeAndOffset () {
 396  151
                 final int int12 = 12;
 397  151
                 final int int23 = 23;
 398  151
                 final int int31 = 31;
 399  151
                 final int int59 = 59;
 400  
 
 401  151
                 if (!this.isYearAsterix && this.year < 0) {
 402  0
                         throw new NumberFormatException("Year must be positive.");
 403  
                 }
 404  
 
 405  151
                 if (this.monthOfYear != -1 && (this.monthOfYear < 1 || this.monthOfYear > int12)) {
 406  1
                         throw new NumberFormatException("Month of year must be in range [1,12].");
 407  
                 }
 408  
 
 409  150
                 if (this.dayOfMonth != -1 && (this.dayOfMonth < 1 || this.dayOfMonth > int31)) {
 410  0
                         throw new NumberFormatException("Day of month must be in range [1,31].");
 411  
                 }
 412  
 
 413  150
                 if (this.hourOfDay != -1 && (this.hourOfDay < 0 || this.hourOfDay > int23)) {
 414  0
                         throw new NumberFormatException("Hour of day must be in range [0,23].");
 415  
                 }
 416  
 
 417  150
                 if (this.minuteOfHour != -1 && (this.minuteOfHour < 0 || this.minuteOfHour > int59)) {
 418  0
                         throw new NumberFormatException("Minute of hour must be in range [0,59].");
 419  
                 }
 420  
 
 421  150
                 if (this.secondOfMinute != -1 && (this.secondOfMinute < 0 || this.secondOfMinute > int59)) {
 422  0
                         throw new NumberFormatException("Second of minute must be in range [0,59].");
 423  
                 }
 424  
 
 425  150
                 if (this.hasZone && (this.zone[1] < 0 || this.zone[1] > int59)) {
 426  0
                         throw new NumberFormatException("Minute of hour in time zone must be in range [0,59].");
 427  
                 }
 428  150
         }
 429  
 
 430  
         /**
 431  
          * @since 0.3.0
 432  
          */
 433  
         private static int[] parseZone (String utc, String sign, String hours, String minutes) {
 434  40
                 if ("Z".equals(utc)) {
 435  38
                         return new int[]{0, 0};
 436  
                 }
 437  2
                 int h = Integer.parseInt(hours);
 438  2
                 int m = Integer.parseInt(minutes);
 439  2
                 if ("-".equals(sign)) {
 440  1
                         h = -h;
 441  
                 }
 442  2
                 return new int[]{h, m};
 443  
         }
 444  
 
 445  
         //---- Object
 446  
 
 447  
         /**
 448  
          * @since 0.3.0
 449  
          */
 450  
         public boolean equals (Object object) {
 451  81
                 if (object == null) {
 452  0
                         return false;
 453  
                 }
 454  81
                 if (this == object) {
 455  0
                         return true;
 456  
                 }
 457  81
                 if (object instanceof PartialTime) {
 458  81
                         final PartialTime other = (PartialTime) object;
 459  81
                         return
 460  
                                 this.isYearAsterix == other.isYearAsterix &&
 461  
                                 (this.isYearAsterix ? true : this.year == other.year) &&
 462  
                                 this.monthOfYear == other.monthOfYear &&
 463  
                                 this.dayOfMonth == other.dayOfMonth &&
 464  
                                 this.hourOfDay == other.hourOfDay &&
 465  
                                 this.minuteOfHour == other.minuteOfHour &&
 466  
                                 this.secondOfMinute == other.secondOfMinute &&
 467  
                                 Arrays.equals(this.zone, other.zone) &&
 468  
                                 this.hasZone == other.hasZone &&
 469  
                                 (this.defaultZone != null ? this.defaultZone.equals(other.defaultZone) : true) &&
 470  
                                 (other.defaultZone != null ? other.defaultZone.equals(this.defaultZone) : true);
 471  
                 }
 472  0
                 return false;
 473  
         }
 474  
 
 475  
         /**
 476  
          * @since 0.3.0
 477  
          */
 478  
         public int hashCode () {
 479  0
                 return
 480  
                         (this.isYearAsterix ? 1 : Integer.valueOf(this.year).hashCode()) *
 481  
                         Integer.valueOf(this.monthOfYear).hashCode() *
 482  
                         Integer.valueOf(this.dayOfMonth).hashCode() *
 483  
                         Integer.valueOf(this.hourOfDay).hashCode() *
 484  
                         Integer.valueOf(this.minuteOfHour).hashCode() *
 485  
                         Integer.valueOf(this.secondOfMinute).hashCode() *
 486  
                         this.zone.hashCode() *
 487  
                         (this.defaultZone == null ? 1 : this.defaultZone.hashCode());
 488  
         }
 489  
 
 490  
         /**
 491  
          *
 492  
          * @since 0.3.0
 493  
          */
 494  
         public String toString () {
 495  14
                 String t =
 496  
                         writePart(this.year) +
 497  
                         "-" +
 498  
                         writePart(this.monthOfYear) +
 499  
                         "-" +
 500  
                         writePart(this.dayOfMonth) +
 501  
                         "T" +
 502  
                         writePart(this.hourOfDay) +
 503  
                         ":" +
 504  
                         writePart(this.minuteOfHour) +
 505  
                         ":" +
 506  
                         writePart(this.secondOfMinute);
 507  
 
 508  14
                 String z = "";
 509  14
                 if (this.hasZone) {
 510  4
                         int hours = this.zone[0];
 511  4
                         int minutes = this.zone[1];
 512  4
                         if (hours == 0 && minutes == 0) {
 513  3
                                 z = "Z";
 514  1
                         } else if (hours < 0) {
 515  1
                                 z = "-" + writePart(-hours) + ":" + writePart(minutes);
 516  
                         } else {
 517  0
                                 z = "+" + writePart(hours) + ":" + writePart(minutes);
 518  
                         }
 519  
                 }
 520  14
                 return t + z;
 521  
         }
 522  
 
 523  
         /**
 524  
          * @since 0.3.0
 525  
          */
 526  
         private String writePart (int i) {
 527  86
                 final int addLeadingZeroIfSmallerThan = 10;
 528  86
                 if (i < 0) {
 529  6
                         return "*";
 530  80
                 } else if (i < addLeadingZeroIfSmallerThan) {
 531  34
                         return "0" + i;
 532  
                 } else {
 533  46
                         return "" + i;
 534  
                 }
 535  
         }
 536  
 
 537  
         /**
 538  
          * @since 0.3.0
 539  
          */
 540  
         private DateTimeZone toDateTimeZone (TimeStamp timeStamp) {
 541  183
                 if (this.hasZone) {
 542  164
                         return DateTimeZone.forOffsetHoursMinutes(this.zone[0], this.zone[1]);
 543  
                 }
 544  19
                 if (this.defaultZone != null) {
 545  19
                         return this.defaultZone;
 546  
                 }
 547  0
                 return TimeUtility.getDateTimeZone(timeStamp);
 548  
         }
 549  
 
 550  
 }