1   package eu.fbk.dkm.pikes.rdf.util;
2   
3   import com.google.common.base.Preconditions;
4   import com.google.common.base.Strings;
5   import eu.fbk.dkm.pikes.rdf.vocab.OWLTIME;
6   import eu.fbk.rdfpro.util.Statements;
7   import org.eclipse.rdf4j.model.Resource;
8   import org.eclipse.rdf4j.model.IRI;
9   import org.eclipse.rdf4j.model.Value;
10  import org.eclipse.rdf4j.model.vocabulary.RDF;
11  import org.eclipse.rdf4j.model.vocabulary.RDFS;
12  import org.eclipse.rdf4j.rio.RDFHandler;
13  import org.eclipse.rdf4j.rio.RDFHandlerException;
14  
15  import javax.annotation.Nullable;
16  import java.util.Calendar;
17  import java.util.GregorianCalendar;
18  import java.util.Objects;
19  import java.util.regex.Pattern;
20  
21  public class OWLTime {
22  
23      private static int parseOptInt(final String string) {
24          if (string.contains("X")) {
25              return -1;
26          }
27          return Integer.parseInt(string);
28      }
29  
30      private static String formatOptInt(final int value, final boolean fourDigits) {
31          return fourDigits ? value == -1 ? "XXXX" : String.format("%04d", value)
32                  : value == -1 ? "XX" : String.format("%02d", value);
33      }
34  
35      private static void emit(final RDFHandler handler, final Resource subj, final IRI pred,
36              final Object obj, final Resource ctx) throws RDFHandlerException {
37          final Value o = Statements.convert(obj, Value.class);
38          if (subj != null && pred != null && o != null) {
39              if (ctx == null) {
40                  handler.handleStatement(Statements.VALUE_FACTORY.createStatement(subj, pred, o));
41              } else {
42                  handler.handleStatement(Statements.VALUE_FACTORY.createStatement(subj, pred, o,
43                          ctx));
44              }
45          }
46      }
47  
48      public static final class Interval {
49  
50          private static final Pattern DATE_TIME_PATTERN = Pattern
51                  .compile("(?:PRESENT_REF|PAST_REF|FUTURE_REF|[0-9X]{4}"
52                          + "(?:-(?:[0-9X]{2}|W[0-9X]{2}|SP|SU|FA|WI)(?:-(?:[0-9X]{2}|WE))?)?)?"
53                          + "T?(?:MO|MI|AF|EV|NI|PM|DT|[0-9X]{2}(?:\\:[0-9X]{2}(?:\\:[0-9X]{2})?)?)?");
54  
55          private static final Interval UNKNOWN = new Interval(null, null);
56  
57          @Nullable
58          private final DateTime begin;
59  
60          @Nullable
61          private final DateTime end;
62  
63          private Interval(@Nullable final DateTime begin, @Nullable final DateTime end) {
64              this.begin = begin;
65              this.end = end;
66          }
67  
68          public static Interval create(@Nullable final DateTime dateTime) {
69              return dateTime == null ? UNKNOWN : new Interval(dateTime, dateTime);
70          }
71  
72          public static Interval create(@Nullable final DateTime begin, @Nullable final DateTime end) {
73              return begin == null && end == null ? UNKNOWN : new Interval(begin, end);
74  
75          }
76  
77          public static Interval create(@Nullable final Interval begin, @Nullable final Interval end) {
78              return create(begin == null ? null : begin.getBegin(),
79                      end == null ? null : end.getEnd());
80          }
81  
82          public static Interval parseTimex(final String value) {
83  
84              // Allocate variables for the various date/time components (-1 = unknown)
85              int century = -1;
86              int decade = -1;
87              int year = -1;
88              String season = null;
89              int month = -1;
90              int week = -1;
91              boolean weekend = false;
92              int day = -1;
93              int hour = -1;
94              int minute = -1;
95              int second = -1;
96  
97              // Normalize the value and split it in its date and time parts
98              final String normValue = value.trim().toUpperCase();
99              if (normValue.isEmpty() || !DATE_TIME_PATTERN.matcher(normValue).matches()) {
100                 return null;
101             }
102             final int timeIndex = normValue.endsWith("_REF") ? -1 : normValue.indexOf('T');
103             final String timePart = timeIndex >= 0 ? normValue.substring(timeIndex + 1) : null;
104             final String datePart = timeIndex >= 0 ? normValue.substring(0, timeIndex) : normValue
105                     .contains(":") ? null : normValue;
106 
107             // Process the date part
108             if (!Strings.isNullOrEmpty(datePart)) {
109                 if (datePart.equals("PRESENT_REF") || datePart.equals("PAST_REF")
110                         || datePart.equals("FUTURE_REF")) {
111                     // TODO
112                 } else {
113                     final String[] tokens = datePart.split("-");
114                     if (Character.isDigit(tokens[0].charAt(0))
115                             && !Character.isDigit(tokens[0].charAt(tokens[0].length() - 1))) {
116                         if (tokens[0].length() == 4 && Character.isDigit(tokens[0].charAt(1))) {
117                             if (Character.isDigit(tokens[0].charAt(2))) {
118                                 decade = Integer.parseInt(tokens[0].substring(0, 3)) * 10;
119                             } else {
120                                 century = Integer.parseInt(tokens[0].substring(0, 2)) * 100;
121                             }
122                         } else {
123                             return null;
124                         }
125                     } else {
126                         year = parseOptInt(tokens[0]);
127                         if (tokens.length >= 2) {
128                             if (!Character.isDigit(tokens[1].charAt(tokens[1].length() - 1))) {
129                                 season = tokens[1]; // SP SU FA WI
130                             } else if (tokens[1].charAt(0) == 'W') {
131                                 week = parseOptInt(tokens[1].substring(1));
132                                 if (tokens.length >= 3 && tokens[2].equals("WE")) {
133                                     weekend = true;
134                                 }
135                             } else {
136                                 month = parseOptInt(tokens[1]);
137                                 day = tokens.length >= 3 ? parseOptInt(tokens[2]) : -1;
138                             }
139                         }
140                     }
141                 }
142             }
143 
144             // Process the time part
145             if (timePart != null) {
146                 if (timePart.equals("MO") || timePart.equals("MI") || timePart.equals("AF")
147                         || timePart.equals("EV") || timePart.equals("NI") || timePart.equals("PM")
148                         || timePart.equals("DT")) {
149                     // TODO: handle periods of day
150                 } else {
151                     final String[] tokens = timePart.split(":");
152                     hour = parseOptInt(tokens[0]);
153                     if (tokens.length >= 2) {
154                         minute = parseOptInt(tokens[1]);
155                         if (tokens.length >= 3) {
156                             second = parseOptInt(tokens[2]);
157                         }
158                     }
159                 }
160             }
161 
162             // Build the interval
163             final String fieldMsg = "Unexpected date/time field(s)";
164             if (century != -1) {
165                 Preconditions.checkArgument(decade == -1 && year == -1 && season == null
166                         && month == -1 && week == -1 && !weekend && day == -1 && hour == -1
167                         && minute == -1 && second == -1, fieldMsg);
168                 final DateTime begin = DateTime.create(century, 1, -1, 1, -1, -1, -1);
169                 final DateTime end = DateTime.create(century + 99, 12, -1, 31, -1, -1, -1);
170                 return create(begin, end);
171 
172             } else if (decade != -1) {
173                 Preconditions.checkArgument(year == -1 && season == null && month == -1
174                         && week == -1 && !weekend && day == -1 && hour == -1 && minute == -1
175                         && second == -1, fieldMsg);
176                 final DateTime begin = DateTime.create(decade, 1, -1, 1, -1, -1, -1);
177                 final DateTime end = DateTime.create(decade + 9, 12, -1, 31, -1, -1, -1);
178                 return create(begin, end);
179 
180             } else if (season != null) {
181                 Preconditions.checkArgument(month == -1 && week == -1 && !weekend && day == -1
182                         && hour == -1 && minute == -1 && second == -1, fieldMsg);
183                 // for simplicity sake, we consider here the following season boundaries: March
184                 // 20, June 21, September 22 and December 21
185                 DateTime begin;
186                 DateTime end;
187                 if (season.equals("SP")) {
188                     begin = DateTime.create(year - 1, 12, -1, 21, -1, -1, -1);
189                     end = DateTime.create(year, 3, -1, 19, -1, -1, -1);
190                 } else if (season.equals("SU")) {
191                     begin = DateTime.create(year, 3, -1, 20, -1, -1, -1);
192                     end = DateTime.create(year, 6, -1, 20, -1, -1, -1);
193                 } else if (season.equals("FA")) {
194                     begin = DateTime.create(year, 6, -1, 21, -1, -1, -1);
195                     end = DateTime.create(year, 9, -1, 21, -1, -1, -1);
196                 } else if (season.equals("WI")) {
197                     begin = DateTime.create(year, 9, -1, 22, -1, -1, -1);
198                     end = DateTime.create(year, 12, -1, 29, -1, -1, -1);
199                 } else {
200                     throw new IllegalArgumentException("Unexpected season ID: " + season);
201                 }
202                 return create(begin, end);
203 
204             } else if (weekend) {
205                 Preconditions.checkArgument(year != -1 && week != -1 && month == -1 && day == -1
206                         && hour == -1 && minute == -1 && second == -1, fieldMsg);
207                 final GregorianCalendar c = new GregorianCalendar();
208                 c.setFirstDayOfWeek(Calendar.MONDAY);
209                 c.setMinimalDaysInFirstWeek(1);
210                 c.set(Calendar.YEAR, year);
211                 c.set(Calendar.WEEK_OF_YEAR, week);
212                 c.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
213                 final DateTime begin = DateTime.create(c.get(Calendar.YEAR),
214                         c.get(Calendar.MONTH) + 1, c.get(Calendar.WEEK_OF_YEAR),
215                         c.get(Calendar.DAY_OF_MONTH), -1, -1, -1);
216                 c.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
217                 final DateTime end = DateTime.create(c.get(Calendar.YEAR),
218                         c.get(Calendar.MONTH) + 1, c.get(Calendar.WEEK_OF_YEAR),
219                         c.get(Calendar.DAY_OF_MONTH), -1, -1, -1);
220                 return create(begin, end);
221 
222             } else {
223                 return create(DateTime.create(year, month, week, day, hour, minute, second));
224             }
225         }
226 
227         public boolean isDateTimeInterval() {
228             return this.begin != null && this.end != null && this.begin.equals(this.end);
229         }
230 
231         @Nullable
232         public DateTime getBegin() {
233             return this.begin;
234         }
235 
236         @Nullable
237         public DateTime getEnd() {
238             return this.end;
239         }
240 
241         @Override
242         public boolean equals(final Object object) {
243             if (object == this) {
244                 return true;
245             }
246             if (!(object instanceof Interval)) {
247                 return false;
248             }
249             final Interval other = (Interval) object;
250             return Objects.equals(this.begin, other.begin) && Objects.equals(this.end, other.end);
251         }
252 
253         @Override
254         public int hashCode() {
255             return Objects.hash(this.begin, this.end);
256         }
257 
258         public IRI toRDF(final RDFHandler handler, final String namespace, final Resource ctx)
259                 throws RDFHandlerException {
260             final IRI iri = toIRI(namespace);
261             if (isDateTimeInterval()) {
262                 final IRI iriDesc = this.begin.toRDF(handler, namespace, ctx);
263                 emit(handler, iri, OWLTIME.HAS_DATE_TIME_DESCRIPTION, iriDesc, ctx);
264                 emit(handler, iri, RDF.TYPE, OWLTIME.DATE_TIME_INTERVAL, ctx);
265             } else {
266                 final IRI beginIRI = this.begin == null ? null : create(this.begin).toRDF(handler,
267                         namespace, ctx);
268                 final IRI endIRI = this.end == null ? null : create(this.end).toRDF(handler,
269                         namespace, ctx);
270                 emit(handler, iri, OWLTIME.INTERVAL_STARTED_BY, beginIRI, ctx);
271                 emit(handler, iri, OWLTIME.INTERVAL_FINISHED_BY, endIRI, ctx);
272             }
273             emit(handler, iri, RDF.TYPE, OWLTIME.PROPER_INTERVAL, ctx);
274             emit(handler, iri, RDFS.LABEL, toString(), ctx);
275             return iri;
276         }
277 
278         public IRI toIRI(final String namespace) {
279             final String localName = toString().replace(" - ", "_").replace(':', '.');
280             return Statements.VALUE_FACTORY.createIRI(namespace, localName);
281         }
282 
283         @Override
284         public String toString() {
285             if (isDateTimeInterval()) {
286                 return this.begin.toString();
287             } else {
288                 return (this.begin == null ? "null" : this.begin.toString()) + " - "
289                         + (this.end == null ? "null" : this.end.toString());
290             }
291         }
292 
293     }
294 
295     public static final class DateTime {
296 
297         private final int year;
298 
299         private final int month;
300 
301         private final int week;
302 
303         private final int day;
304 
305         private final int dayOfWeek;
306 
307         private final int hour;
308 
309         private final int minute;
310 
311         private final int second;
312 
313         private DateTime(final int year, final int month, final int week, final int day,
314                 final int dayOfWeek, final int hour, final int minute, final int second) {
315             this.year = year;
316             this.month = month;
317             this.week = week;
318             this.day = day;
319             this.dayOfWeek = dayOfWeek;
320             this.hour = hour;
321             this.minute = minute;
322             this.second = second;
323         }
324 
325         public static DateTime create(final int year, int month, int week, final int day,
326                 final int hour, final int minute, final int second) {
327 
328             // Derive month from week, or week from month and day; also derive day of week
329             int dayOfWeek = -1;
330             if (year != -1 && (week != -1 || month != -1 && day != -1)) {
331                 final GregorianCalendar c = new GregorianCalendar();
332                 c.setFirstDayOfWeek(Calendar.MONDAY);
333                 c.setMinimalDaysInFirstWeek(1);
334                 c.set(Calendar.YEAR, year);
335                 if (month != -1 && day != -1) {
336                     c.set(Calendar.MONTH, month - 1);
337                     c.set(Calendar.DAY_OF_MONTH, day);
338                     dayOfWeek = c.get(Calendar.DAY_OF_WEEK);
339                     if (week == -1) {
340                         week = c.get(Calendar.WEEK_OF_YEAR);
341                     }
342                 } else { // week != -1
343                     if (day != -1) {
344                         c.set(Calendar.DAY_OF_MONTH, day);
345                         for (int i = 0; i < 12; ++i) {
346                             c.set(Calendar.MONTH, i);
347                             if (c.get(Calendar.WEEK_OF_YEAR) == week) {
348                                 month = i + 1;
349                                 dayOfWeek = c.get(Calendar.DAY_OF_WEEK);
350                                 break;
351                             }
352                         }
353                     } else if (week == 1) {
354                         month = 1;
355                     } else {
356                         c.set(Calendar.WEEK_OF_YEAR, week);
357                         c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
358                         month = c.get(Calendar.MONTH) + 1;
359                     }
360                 }
361             }
362 
363             return new DateTime(year, month, week, day, dayOfWeek, hour, minute, second);
364         }
365 
366         public int getYear() {
367             return this.year;
368         }
369 
370         public int getMonth() {
371             return this.month;
372         }
373 
374         public int getWeek() {
375             return this.week;
376         }
377 
378         public int getDay() {
379             return this.day;
380         }
381 
382         public int getDayOfWeek() {
383             return this.dayOfWeek;
384         }
385 
386         public int getHour() {
387             return this.hour;
388         }
389 
390         public int getMinute() {
391             return this.minute;
392         }
393 
394         public int getSecond() {
395             return this.second;
396         }
397 
398         @Override
399         public boolean equals(final Object object) {
400             if (object == this) {
401                 return true;
402             }
403             if (!(object instanceof DateTime)) {
404                 return false;
405             }
406             final DateTime other = (DateTime) object;
407             return this.year == other.year && this.month == other.month && this.week == other.week
408                     && this.day == other.day && this.hour == other.hour
409                     && this.minute == other.minute && this.second == other.second;
410         }
411 
412         @Override
413         public int hashCode() {
414             return Objects.hash(this.year, this.month, this.week, this.day, this.hour,
415                     this.minute, this.second);
416         }
417 
418         public IRI toRDF(final RDFHandler handler, final String namespace,
419                 @Nullable final Resource ctx) throws RDFHandlerException {
420 
421             // TODO: this implementation allows for missing fields: should we allow them?
422 
423             // Emit RDF
424             final IRI iri = toIRI(namespace);
425             IRI unitType = null;
426             emit(handler, iri, RDF.TYPE, OWLTIME.DATE_TIME_DESCRIPTION, ctx);
427             if (this.year != -1) {
428                 emit(handler, iri, OWLTIME.YEAR, this.year, ctx);
429                 unitType = OWLTIME.UNIT_YEAR;
430             }
431             if (this.month != -1) {
432                 emit(handler, iri, OWLTIME.MONTH, this.month, ctx);
433                 unitType = OWLTIME.UNIT_MONTH;
434             }
435             if (this.week != -1) {
436                 emit(handler, iri, OWLTIME.WEEK, this.week, ctx);
437                 unitType = OWLTIME.UNIT_WEEK;
438             }
439             if (this.day != -1) {
440                 emit(handler, iri, OWLTIME.DAY, this.day, ctx);
441                 unitType = OWLTIME.UNIT_DAY;
442             }
443             if (this.dayOfWeek != -1) {
444                 IRI dayIRI;
445                 if (this.dayOfWeek == Calendar.MONDAY) {
446                     dayIRI = OWLTIME.MONDAY;
447                 } else if (this.dayOfWeek == Calendar.TUESDAY) {
448                     dayIRI = OWLTIME.TUESDAY;
449                 } else if (this.dayOfWeek == Calendar.WEDNESDAY) {
450                     dayIRI = OWLTIME.WEDNESDAY;
451                 } else if (this.dayOfWeek == Calendar.THURSDAY) {
452                     dayIRI = OWLTIME.THURSDAY;
453                 } else if (this.dayOfWeek == Calendar.FRIDAY) {
454                     dayIRI = OWLTIME.FRIDAY;
455                 } else if (this.dayOfWeek == Calendar.SATURDAY) {
456                     dayIRI = OWLTIME.SATURDAY;
457                 } else {
458                     dayIRI = OWLTIME.SUNDAY;
459                 }
460                 emit(handler, iri, OWLTIME.DAY_OF_WEEK, dayIRI, ctx);
461             }
462             if (this.hour != -1) {
463                 emit(handler, iri, OWLTIME.HOUR, this.hour, ctx);
464                 unitType = OWLTIME.UNIT_HOUR;
465             }
466             if (this.minute != -1) {
467                 emit(handler, iri, OWLTIME.MINUTE, this.minute, ctx);
468                 unitType = OWLTIME.UNIT_MINUTE;
469             }
470             if (this.second != -1) {
471                 emit(handler, iri, OWLTIME.SECOND, this.second, ctx);
472                 unitType = OWLTIME.UNIT_SECOND;
473             }
474             emit(handler, iri, OWLTIME.UNIT_TYPE, unitType, ctx);
475             return iri;
476         }
477 
478         public IRI toIRI(final String namespace) {
479             return Statements.VALUE_FACTORY.createIRI(namespace, toString().replace(':', '.')
480                     + "_desc");
481         }
482 
483         @Override
484         public String toString() {
485             // Build the label
486             final StringBuilder builder = new StringBuilder();
487             final boolean hasDate = this.year != -1 || this.month != -1 || this.week != -1
488                     || this.day != -1;
489             final boolean hasTime = this.hour != -1 || this.minute != -1 || this.second != -1;
490             if (hasDate) {
491                 builder.append(formatOptInt(this.year, true));
492                 if (this.week != -1 && this.day == -1) {
493                     builder.append("-W").append(formatOptInt(this.week, false));
494                 } else if (hasTime || this.month != -1 || this.day != -1) {
495                     builder.append('-').append(formatOptInt(this.month, false));
496                     if (hasTime || this.day != -1) {
497                         builder.append('-').append(formatOptInt(this.day, false));
498                     }
499                 }
500             }
501             if (hasTime) {
502                 if (hasDate) {
503                     builder.append('T');
504                 }
505                 builder.append(formatOptInt(this.hour, false));
506                 if (this.minute != -1 || this.second != -1) {
507                     builder.append(':').append(formatOptInt(this.minute, false));
508                     if (this.second != -1) {
509                         builder.append(':').append(formatOptInt(this.second, false));
510                     }
511                 }
512             }
513             return builder.toString();
514         }
515 
516     }
517 
518     public static final class Duration {
519 
520         private static final Pattern DURATION_PATTERN = Pattern
521                 .compile("P(?:[0-9X]+Y)?(?:[0-9X]+M)?(?:[0-9X]+W)?(?:[0-9X]+D)?"
522                         + "(?:T(?:[0-9X]+H)?(?:[0-9X]+M)?(?:[0-9X]+S)?)?");
523 
524         private final int years;
525 
526         private final int months;
527 
528         private final int weeks;
529 
530         private final int days;
531 
532         private final int hours;
533 
534         private final int minutes;
535 
536         private final int seconds;
537 
538         private Duration(final int years, final int months, final int weeks, final int days,
539                 final int hours, final int minutes, final int seconds) {
540             this.years = years;
541             this.months = months;
542             this.weeks = weeks;
543             this.days = days;
544             this.hours = hours;
545             this.minutes = minutes;
546             this.seconds = seconds;
547         }
548 
549         public static Duration create(final int years, final int months, final int weeks,
550                 final int days, final int hours, final int minutes, final int seconds) {
551             Preconditions.checkArgument(years >= 0);
552             Preconditions.checkArgument(months >= 0);
553             Preconditions.checkArgument(weeks >= 0);
554             Preconditions.checkArgument(days >= 0);
555             Preconditions.checkArgument(hours >= 0);
556             Preconditions.checkArgument(minutes >= 0);
557             Preconditions.checkArgument(seconds >= 0);
558             return new Duration(years, months, weeks, days, hours, minutes, seconds);
559         }
560 
561         @Nullable
562         public static Duration parseTimex(final String value) {
563 
564             final String normalizedValue = value.trim().toUpperCase();
565             if (!DURATION_PATTERN.matcher(value).matches()) {
566                 return null;
567             }
568 
569             int years = 0;
570             int months = 0;
571             int weeks = 0;
572             int days = 0;
573             int hours = 0;
574             int minutes = 0;
575             int seconds = 0;
576             boolean unknown = true;
577 
578             try {
579                 Preconditions.checkArgument(normalizedValue.startsWith("P"));
580                 int start = 1;
581                 boolean insideTime = false;
582                 for (int i = 1; i < normalizedValue.length(); ++i) {
583                     final char ch = normalizedValue.charAt(i);
584                     if (ch == 'T') {
585                         insideTime = true;
586                         start = i + 1;
587                     } else if (!Character.isDigit(ch) && ch != 'X') {
588                         final int v = parseOptInt(normalizedValue.substring(start, i));
589                         unknown &= v == -1;
590                         if (v > 0) {
591                             if (ch == 'Y') {
592                                 years = v;
593                             } else if (ch == 'M' && !insideTime) {
594                                 months = v;
595                             } else if (ch == 'W') {
596                                 weeks = v;
597                             } else if (ch == 'D') {
598                                 days = v;
599                             } else if (ch == 'H') {
600                                 hours = v;
601                             } else if (ch == 'M' && insideTime) {
602                                 minutes = v;
603                             } else if (ch == 'S') {
604                                 seconds = v;
605                             } else {
606                                 throw new IllegalArgumentException("Invalid flag " + ch + " in "
607                                         + value);
608                             }
609                         }
610                         start = i + 1;
611                     }
612                 }
613             } catch (final Throwable ex) {
614                 throw new IllegalArgumentException("Invalid duration string: '" + value + "'", ex);
615             }
616 
617             // TODO: we currently ignore expressions such as PXD (some days) and limit ourselves
618             // to precisely quantified expressions. Still, it would be important to represent this
619             // kind of information in a fuzzy way (e.g. P1D <= PXD <= P10D)
620             if (unknown) {
621                 return null;
622             }
623 
624             return create(years, months, weeks, days, hours, minutes, seconds);
625         }
626 
627         public int getYears() {
628             return this.years;
629         }
630 
631         public int getMinutes() {
632             return this.minutes;
633         }
634 
635         public int getWeeks() {
636             return this.weeks;
637         }
638 
639         public int getDays() {
640             return this.days;
641         }
642 
643         public int getHours() {
644             return this.hours;
645         }
646 
647         public int getMonths() {
648             return this.months;
649         }
650 
651         public int getSeconds() {
652             return this.seconds;
653         }
654 
655         @Override
656         public boolean equals(final Object object) {
657             if (object == this) {
658                 return true;
659             }
660             if (!(object instanceof Duration)) {
661                 return false;
662             }
663             final Duration other = (Duration) object;
664             return this.years == other.years && this.months == other.months
665                     && this.weeks == other.weeks && this.days == other.days
666                     && this.hours == other.hours && this.minutes == other.minutes
667                     && this.seconds == other.seconds;
668         }
669 
670         @Override
671         public int hashCode() {
672             return Objects.hash(this.years, this.months, this.weeks, this.days, this.hours,
673                     this.minutes, this.seconds);
674         }
675 
676         public IRI toRDF(final RDFHandler handler, final String namespace,
677                 @Nullable final Resource ctx) throws RDFHandlerException {
678             final IRI iri = toIRI(namespace);
679             emit(handler, iri, RDF.TYPE, OWLTIME.DURATION_DESCRIPTION, ctx);
680             if (this.years > 0) {
681                 emit(handler, iri, OWLTIME.YEARS, this.years, ctx);
682             }
683             if (this.months > 0) {
684                 emit(handler, iri, OWLTIME.MONTHS, this.months, ctx);
685             }
686             if (this.weeks > 0) {
687                 emit(handler, iri, OWLTIME.WEEKS, this.weeks, ctx);
688             }
689             if (this.days > 0) {
690                 emit(handler, iri, OWLTIME.YEARS, this.days, ctx);
691             }
692             if (this.hours > 0) {
693                 emit(handler, iri, OWLTIME.HOURS, this.hours, ctx);
694             }
695             if (this.minutes > 0) {
696                 emit(handler, iri, OWLTIME.MINUTES, this.minutes, ctx);
697             }
698             if (this.seconds > 0) {
699                 emit(handler, iri, OWLTIME.SECONDS, this.seconds, ctx);
700             }
701             emit(handler, iri, RDFS.LABEL, toString(), ctx);
702             return iri;
703         }
704 
705         public IRI toIRI(final String namespace) {
706             return Statements.VALUE_FACTORY.createIRI(namespace, toString() + "_desc");
707         }
708 
709         @Override
710         public String toString() {
711             final StringBuilder builder = new StringBuilder("P");
712             if (this.years > 0) {
713                 builder.append(this.years).append('Y');
714             }
715             if (this.months > 0) {
716                 builder.append(this.months).append('M');
717             }
718             if (this.weeks > 0) {
719                 builder.append(this.weeks).append('W');
720             }
721             if (this.days > 0) {
722                 builder.append(this.days).append('D');
723             }
724             if (this.hours > 0 || this.minutes > 0 || this.seconds > 0) {
725                 builder.append('T');
726             }
727             if (this.hours > 0) {
728                 builder.append(this.hours).append('H');
729             }
730             if (this.minutes > 0) {
731                 builder.append(this.minutes).append('M');
732             }
733             if (this.seconds > 0) {
734                 builder.append(this.seconds).append('S');
735             }
736             return builder.toString();
737         }
738 
739     }
740 
741 }