• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

evolvedbinary / elemental / 982

29 Apr 2025 08:34PM UTC coverage: 56.409% (+0.007%) from 56.402%
982

push

circleci

adamretter
[feature] Improve README.md badges

28451 of 55847 branches covered (50.94%)

Branch coverage included in aggregate %.

77468 of 131924 relevant lines covered (58.72%)

0.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.55
/exist-core/src/main/java/org/exist/xquery/value/AbstractDateTimeValue.java
1
/*
2
 * Elemental
3
 * Copyright (C) 2024, Evolved Binary Ltd
4
 *
5
 * admin@evolvedbinary.com
6
 * https://www.evolvedbinary.com | https://www.elemental.xyz
7
 *
8
 * Use of this software is governed by the Business Source License 1.1
9
 * included in the LICENSE file and at www.mariadb.com/bsl11.
10
 *
11
 * Change Date: 2028-04-27
12
 *
13
 * On the date above, in accordance with the Business Source License, use
14
 * of this software will be governed by the Apache License, Version 2.0.
15
 *
16
 * Additional Use Grant: Production use of the Licensed Work for a permitted
17
 * purpose. A Permitted Purpose is any purpose other than a Competing Use.
18
 * A Competing Use means making the Software available to others in a commercial
19
 * product or service that: substitutes for the Software; substitutes for any
20
 * other product or service we offer using the Software that exists as of the
21
 * date we make the Software available; or offers the same or substantially
22
 * similar functionality as the Software.
23
 *
24
 * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
25
 *       The original license header is included below.
26
 *
27
 * =====================================================================
28
 *
29
 * eXist-db Open Source Native XML Database
30
 * Copyright (C) 2001 The eXist-db Authors
31
 *
32
 * info@exist-db.org
33
 * http://www.exist-db.org
34
 *
35
 * This library is free software; you can redistribute it and/or
36
 * modify it under the terms of the GNU Lesser General Public
37
 * License as published by the Free Software Foundation; either
38
 * version 2.1 of the License, or (at your option) any later version.
39
 *
40
 * This library is distributed in the hope that it will be useful,
41
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
42
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
43
 * Lesser General Public License for more details.
44
 *
45
 * You should have received a copy of the GNU Lesser General Public
46
 * License along with this library; if not, write to the Free Software
47
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
48
 */
49
package org.exist.xquery.value;
50

51
import com.ibm.icu.text.Collator;
52
import org.apache.logging.log4j.LogManager;
53
import org.apache.logging.log4j.Logger;
54
import org.apache.xerces.util.DatatypeMessageFormatter;
55
import org.exist.xquery.Constants;
56
import org.exist.xquery.Constants.Comparison;
57
import org.exist.xquery.ErrorCodes;
58
import org.exist.xquery.Expression;
59
import org.exist.xquery.XPathException;
60

61
import javax.xml.datatype.DatatypeConstants;
62
import javax.xml.datatype.Duration;
63
import javax.xml.datatype.XMLGregorianCalendar;
64
import javax.xml.namespace.QName;
65
import java.math.BigDecimal;
66
import java.math.BigInteger;
67
import java.text.DecimalFormat;
68
import java.time.Instant;
69
import java.util.Calendar;
70
import java.util.Date;
71
import java.util.GregorianCalendar;
72
import java.util.TimeZone;
73
import java.util.regex.Matcher;
74
import java.util.regex.Pattern;
75

76
/**
77
 * @author <a href="mailto:adam@evolvedbinary.com">Adam Retter</a>
78
 * @author wolf
79
 * @author <a href="mailto:piotr@ideanest.com">Piotr Kaminski</a>
80
 * @author ljo
81
 * @author <a href="mailto:adam@evolvedbinary.com">Adam Retter</a>
82
 */
83
public abstract class AbstractDateTimeValue extends ComputableValue {
84

85
    public final static int YEAR = 0;
86
    public final static int MONTH = 1;
87
    public final static int DAY = 2;
88
    public final static int HOUR = 3;
89
    public final static int MINUTE = 4;
90
    public final static int SECOND = 5;
91
    public final static int MILLISECOND = 6;
92
    protected static final Pattern negativeDateStart = Pattern.compile("^\\d\\d?-(\\d+)-(.*)");
1✔
93
    protected static final short[] monthData = {306, 337, 0, 31, 61, 92, 122, 153, 184, 214, 245, 275};
1✔
94
    private final static Logger LOG = LogManager.getLogger(AbstractDateTimeValue.class);
1✔
95
    private static final Duration tzLowerBound = TimeUtils.getInstance().newDurationDayTime("-PT14H");
1✔
96
    private static final Duration tzUpperBound = tzLowerBound.negate();
1✔
97
    protected static byte[] daysPerMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
1✔
98
    //Provisionally public
99
    public final XMLGregorianCalendar calendar;
100
    private XMLGregorianCalendar implicitCalendar, canonicalCalendar, trimmedCalendar;
101

102
    /**
103
     * Create a new date time value based on the given calendar.  The calendar is
104
     * <em>not</em> cloned, so it is the subclass's responsibility to make sure there are
105
     * no external references to it that would allow for mutation.
106
     *
107
     * @param calendar the calendar to wrap into an XPath value
108
     */
109
    protected AbstractDateTimeValue(final XMLGregorianCalendar calendar) {
110
        this(null, calendar);
×
111
    }
×
112

113
    protected AbstractDateTimeValue(final Expression expression, XMLGregorianCalendar calendar) {
114
        super(expression);
1✔
115
        this.calendar = calendar;
1✔
116
    }
1✔
117

118
    protected AbstractDateTimeValue(final String lexicalValue) throws XPathException {
119
        this(null, lexicalValue);
×
120
    }
×
121

122
    protected AbstractDateTimeValue(final Expression expression, String lexicalValue) throws XPathException {
123
        super(expression);
1✔
124
        lexicalValue = StringValue.trimWhitespace(lexicalValue);
1✔
125

126
        //lexicalValue = normalizeDate(lexicalValue);
127
        //lexicalValue = normalizeTime(getType(), lexicalValue);
128
        try {
129
            calendar = parse(lexicalValue);
1✔
130
        } catch (final IllegalArgumentException e) {
1✔
131
            throw new XPathException(getExpression(), ErrorCodes.FORG0001, "illegal lexical form for date-time-like value '" + lexicalValue + "' " + e.getMessage(), e);
1✔
132
        }
133
    }
1✔
134

135
    /**
136
     * Utility method that is able to clone a calendar whose year is 0
137
     * (whatever a year 0 means).
138
     * It looks like the JDK is unable to do that.
139
     *
140
     * @param calendar The Calendar to clone
141
     * @return the cloned Calendar
142
     */
143
    public static XMLGregorianCalendar cloneXMLGregorianCalendar(XMLGregorianCalendar calendar) {
144
        boolean hacked = false;
1✔
145
        if (calendar.getYear() == 0) {
1!
146
            calendar.setYear(1);
×
147
            hacked = true;
×
148
        }
149
        final XMLGregorianCalendar result = (XMLGregorianCalendar) calendar.clone();
1✔
150
        if (hacked) {
1!
151
            //reset everything
152
            calendar.setYear(0);
×
153
            //-1 could also be considered
154
            result.setYear(0);
×
155
        }
156
        return result;
1✔
157
    }
158

159
    private static boolean isDigit(char ch) {
160
        return '0' <= ch && ch <= '9';
1✔
161
    }
162

163
    /**
164
     * Calculate the Julian day number at 00:00 on a given date. Code taken from saxon,
165
     * see <a href="http://saxon.sourceforge.net">http://saxon.sourceforge.net</a>
166
     * Original algorithm is taken from
167
     * http://vsg.cape.com/~pbaum/date/jdalg.htm and
168
     * http://vsg.cape.com/~pbaum/date/jdalg2.htm
169
     * (adjusted to handle BC dates correctly)
170
     *
171
     * Note that this assumes dates in the proleptic Gregorian calendar.
172
     *
173
     * @param year  the year
174
     * @param month the month (1-12)
175
     * @param day   the day (1-31)
176
     * @return the Julian day number
177
     */
178
    public static int getJulianDayNumber(int year, int month, int day) {
179
        int z = year - (month < 3 ? 1 : 0);
×
180
        final short f = monthData[month - 1];
×
181
        if (z >= 0) {
×
182
            return day + f + 365 * z + z / 4 - z / 100 + z / 400 + 1721118;
×
183
        } else {
184
            // for negative years, add 12000 years and then subtract the days!
185
            z += 12000;
×
186
            final int j = day + f + 365 * z + z / 4 - z / 100 + z / 400 + 1721118;
×
187
            return j - (365 * 12000 + 12000 / 4 - 12000 / 100 + 12000 / 400);  // number of leap years in 12000 years
×
188
        }
189
    }
190

191
    /**
192
     * Return a calendar with the timezone field set, to be used for order comparison.
193
     * If the original calendar did not specify a timezone, set the local timezone (unadjusted
194
     * for daylight savings).  The returned calendars will be totally ordered between themselves.
195
     * We also set any missing fields to ensure that normalization doesn't discard important data!
196
     * (This is probably a bug in the JAXP implementation, but the workaround doesn't hurt us,
197
     * so it's faster to just fix it here.)
198
     *
199
     * @return the calendar represented by this object, with the timezone field filled in with an implicit value if necessary
200
     */
201
    protected XMLGregorianCalendar getImplicitCalendar() {
202
        if (implicitCalendar == null) {
1✔
203
            implicitCalendar = (XMLGregorianCalendar) calendar.clone();
1✔
204
            if (calendar.getTimezone() == DatatypeConstants.FIELD_UNDEFINED) {
1✔
205
                implicitCalendar.setTimezone(TimeUtils.getInstance().getLocalTimezoneOffsetMinutes());
1✔
206
            }
207
            // fill in fields from default reference; don't have to worry about weird combinations of fields being set, since we control that on creation
208
            switch (getType()) {
1✔
209
                case Type.DATE:
210
                    implicitCalendar.setTime(0, 0, 0);
1✔
211
                    break;
1✔
212
                case Type.TIME:
213
                    implicitCalendar.setYear(1972);
1✔
214
                    implicitCalendar.setMonth(12);
1✔
215
                    implicitCalendar.setDay(31);
1✔
216
                    break;
217
                default:
218
            }
219
            implicitCalendar = implicitCalendar.normalize();    // the comparison routines will normalize it anyway, just do it once here
1✔
220
        }
221
        return implicitCalendar;
1✔
222
    }
223

224
    // TODO: method not currently used, apparently the XPath spec never needs to canonicalize
225
    // date/times after all (see section 17.1.2 on casting)
226
    protected XMLGregorianCalendar getCanonicalCalendar() {
227
        if (canonicalCalendar == null) {
1!
228
            canonicalCalendar = getTrimmedCalendar().normalize();
1✔
229
        }
230
        return canonicalCalendar;
1✔
231
    }
232

233
    public XMLGregorianCalendar getTrimmedCalendar() {
234
        if (trimmedCalendar == null) {
1✔
235
            trimmedCalendar = cloneXMLGregorianCalendar(calendar);
1✔
236
            final BigDecimal fract = trimmedCalendar.getFractionalSecond();
1✔
237
            if (fract != null) {
1✔
238
                // TODO: replace following algorithm in JDK 1.5 with fract.stripTrailingZeros();
239
                final String s = fract.toString();
1✔
240
                int i = s.length();
1✔
241
                while (i > 0 && s.charAt(i - 1) == '0') i--;
1✔
242
                if (i == 0) {
1✔
243
                    trimmedCalendar.setFractionalSecond(null);
1✔
244
                } else if (i != s.length()) {
1✔
245
                    trimmedCalendar.setFractionalSecond(new BigDecimal(s.substring(0, i)));
1✔
246
                }
247
            }
248
        }
249
        return trimmedCalendar;
1✔
250
    }
251

252
    protected XMLGregorianCalendar getCanonicalOrTrimmedCalendar() {
253
        try {
254
            return getCanonicalCalendar();
×
255
        } catch (final Exception e) {
×
256
            return getTrimmedCalendar();
×
257
        }
258

259
    }
260

261
    protected abstract AbstractDateTimeValue createSameKind(XMLGregorianCalendar cal) throws XPathException;
262

263
    public long getTimeInMillis() {
264
        // use getImplicitCalendar() rather than relying on toGregorianCalendar timezone defaulting
265
        // to maintain consistency
266
        return getImplicitCalendar().toGregorianCalendar().getTimeInMillis();
1✔
267
    }
268

269
    protected abstract QName getXMLSchemaType();
270

271
    public String getStringValue() throws XPathException {
272
        String r = getTrimmedCalendar().toXMLFormat();
1✔
273
        // hacked to match the format mandated in XPath 2 17.1.2, which is different from the XML Schema canonical format
274
        //if (r.charAt(r.length()-1) == 'Z') r = r.substring(0, r.length()-1) + "+00:00";
275

276
        //Let's try these lexical transformations...
277
        final boolean startsWithDashDash = r.startsWith("--");
1✔
278
        r = r.replaceAll("--", "");
1✔
279
        if (startsWithDashDash) {
1✔
280
            r = "--" + r;
1✔
281
        }
282

283
        final Matcher m = negativeDateStart.matcher(r);
1✔
284
        if (m.matches()) {
1!
285
            final int year = Integer.parseInt(m.group(1));
×
286
            final DecimalFormat df = new DecimalFormat("0000");
×
287
            r = "-" + df.format(year) + "-" + m.group(2);
×
288
        }
289

290
        return r;
1✔
291
    }
292

293
    public boolean effectiveBooleanValue() throws XPathException {
294
        throw new XPathException(getExpression(), ErrorCodes.FORG0006, "effective boolean value invalid operand type: " + Type.getTypeName(getType()));
1✔
295
    }
296

297
    public abstract AtomicValue convertTo(int requiredType) throws XPathException;
298

299
    public int getPart(int part) {
300
        switch (part) {
1!
301
            case YEAR:
302
                return calendar.getYear();
1✔
303
            case MONTH:
304
                return calendar.getMonth();
1✔
305
            case DAY:
306
                return calendar.getDay();
1✔
307
            case HOUR:
308
                return calendar.getHour();
1✔
309
            case MINUTE:
310
                return calendar.getMinute();
1✔
311
            case SECOND:
312
                return calendar.getSecond();
1✔
313
            case MILLISECOND:
314
                final int mSec = calendar.getMillisecond();
1✔
315
                if (mSec == DatatypeConstants.FIELD_UNDEFINED) {
1!
316
                    return 0;
×
317
                } else {
318
                    return calendar.getMillisecond();
1✔
319
                }
320
            default:
321
                throw new IllegalArgumentException("Invalid argument to method getPart");
×
322
        }
323
    }
324

325
    /**
326
     * Returns true if a timezone is defined.
327
     *
328
     * @return true if a timezone is defined.
329
     */
330
    public boolean hasTimezone() {
331
        return calendar.getTimezone() != DatatypeConstants.FIELD_UNDEFINED;
1!
332
    }
333

334
    protected void validateTimezone(DayTimeDurationValue offset) throws XPathException {
335
        final Duration tz = offset.duration;
1✔
336
        final Number secs = tz.getField(DatatypeConstants.SECONDS);
1✔
337
        if (secs != null && ((BigDecimal) secs).compareTo(BigDecimal.valueOf(0)) != 0) {
1✔
338
            throw new XPathException(getExpression(), ErrorCodes.FODT0003, "duration " + offset + " has fractional minutes so cannot be used as a timezone offset");
1✔
339
        }
340
        if (!(
341
                tz.equals(tzLowerBound) ||
1!
342
                        tz.equals(tzUpperBound) ||
1✔
343
                        (tz.isLongerThan(tzLowerBound) && tz.isShorterThan(tzUpperBound))
1✔
344
        )) {
345
            throw new XPathException(getExpression(), ErrorCodes.FODT0003, "duration " + offset + " outside valid timezone offset range");
1✔
346
        }
347
    }
1✔
348

349
    public AbstractDateTimeValue adjustedToTimezone(DayTimeDurationValue offset) throws XPathException {
350
        if (offset == null) {
1✔
351
            offset = new DayTimeDurationValue(getExpression(), TimeUtils.getInstance().getLocalTimezoneOffsetMillis());
1✔
352
        }
353
        validateTimezone(offset);
1✔
354
        XMLGregorianCalendar xgc = (XMLGregorianCalendar) calendar.clone();
1✔
355
        if (xgc.getTimezone() != DatatypeConstants.FIELD_UNDEFINED) {
1✔
356
            if (getType() == Type.DATE) {
1✔
357
                xgc.setTime(0, 0, 0);
1✔
358
            }    // set the fields so we don't lose precision when shifting timezones
359
            xgc = xgc.normalize();
1✔
360
            xgc.add(offset.duration);
1✔
361
        }
362
        try {
363
            xgc.setTimezone((int) (offset.getValue() / 60));
1✔
364
        } catch (final IllegalArgumentException e) {
1✔
365
            throw new XPathException(getExpression(), ErrorCodes.FORG0001, "illegal timezone offset " + offset, e);
×
366
        }
367
        return createSameKind(xgc);
1✔
368
    }
369

370
    public AbstractDateTimeValue withoutTimezone() throws XPathException {
371
        final XMLGregorianCalendar xgc = (XMLGregorianCalendar) calendar.clone();
1✔
372
        xgc.setTimezone(DatatypeConstants.FIELD_UNDEFINED);
1✔
373
        return createSameKind(xgc);
1✔
374
    }
375

376
    public Sequence getTimezone() throws XPathException {
377
        final int tz = calendar.getTimezone();
1✔
378
        if (tz == DatatypeConstants.FIELD_UNDEFINED) {
1✔
379
            return Sequence.EMPTY_SEQUENCE;
1✔
380
        }
381
        return new DayTimeDurationValue(getExpression(), tz * 60000L);
1✔
382
    }
383

384
    @Override
385
    public boolean compareTo(Collator collator, Comparison operator, AtomicValue other) throws XPathException {
386
        final int cmp = compareTo(collator, other);
1✔
387
        return switch (operator) {
1!
388
            case EQ -> cmp == 0;
1✔
389
            case NEQ -> cmp != 0;
1✔
390
            case LT -> cmp < 0;
1✔
391
            case LTEQ -> cmp <= 0;
1✔
392
            case GT -> cmp > 0;
1!
393
            case GTEQ -> cmp >= 0;
1✔
394
            default -> throw new XPathException(getExpression(), "Unknown operator type in comparison");
×
395
        };
396
    }
397

398
    public int compareTo(Collator collator, AtomicValue other) throws XPathException {
399
        if (other.getType() == getType() || other.getType() == Type.DATE_TIME_STAMP && getType() == Type.DATE_TIME || other.getType() == Type.DATE_TIME && getType() == Type.DATE_TIME_STAMP) {
1!
400
            // filling in missing timezones with local timezone, should be total order as per XPath 2.0 10.4
401
            final int r = getImplicitCalendar().compare(((AbstractDateTimeValue) other).getImplicitCalendar());
1✔
402
            if (r == DatatypeConstants.INDETERMINATE) {
1!
403
                throw new RuntimeException("indeterminate order between " + this + " and " + other);
×
404
            }
405
            return r;
1✔
406
        }
407
        throw new XPathException(getExpression(), ErrorCodes.XPTY0004, "Type error: cannot compare " + Type.getTypeName(getType())
×
408
                + " to " + Type.getTypeName(other.getType()));
×
409
    }
410

411
    public AtomicValue max(Collator collator, AtomicValue other) throws XPathException {
412
        final AbstractDateTimeValue otherDate = other.getType() == getType() ? (AbstractDateTimeValue) other : (AbstractDateTimeValue) other.convertTo(getType());
1!
413
        return getImplicitCalendar().compare(otherDate.getImplicitCalendar()) > 0 ? this : other;
1✔
414
    }
415

416
    public AtomicValue min(Collator collator, AtomicValue other) throws XPathException {
417
        final AbstractDateTimeValue otherDate = other.getType() == getType() ? (AbstractDateTimeValue) other : (AbstractDateTimeValue) other.convertTo(getType());
1!
418
        return getImplicitCalendar().compare(otherDate.getImplicitCalendar()) < 0 ? this : other;
1✔
419
    }
420

421
    // override for xs:time
422
    public ComputableValue plus(ComputableValue other) throws XPathException {
423
        return switch (other.getType()) {
1!
424
            case Type.YEAR_MONTH_DURATION, Type.DAY_TIME_DURATION -> other.plus(this);
1✔
425
            default -> throw new XPathException(getExpression(),
×
426
                    "Operand to plus should be of type xdt:dayTimeDuration or xdt:yearMonthDuration; got: "
×
427
                            + Type.getTypeName(other.getType()));
×
428
        };
429
    }
430

431
    public ComputableValue mult(ComputableValue other) throws XPathException {
432
        throw new XPathException(getExpression(), "multiplication is not supported for type " + Type.getTypeName(getType()));
×
433
    }
434

435
    public ComputableValue div(ComputableValue other) throws XPathException {
436
        throw new XPathException(getExpression(), "division is not supported for type " + Type.getTypeName(getType()));
×
437
    }
438

439
    public int conversionPreference(Class<?> javaClass) {
440
        if (javaClass.isAssignableFrom(DateValue.class)) {
1✔
441
            return 0;
1✔
442
        }
443
        if (javaClass.isAssignableFrom(XMLGregorianCalendar.class)) {
1✔
444
            return 1;
1✔
445
        }
446
        if (javaClass.isAssignableFrom(GregorianCalendar.class)) {
1✔
447
            return 2;
1✔
448
        }
449
        if (Date.class.equals(javaClass)) {
1✔
450
            return 3;
1✔
451
        }
452
        if (Instant.class.equals(javaClass)) {
1!
453
            return 4;
1✔
454
        }
455
        return Integer.MAX_VALUE;
×
456
    }
457

458
    @Override
459
    public <T> T toJavaObject(Class<T> target) throws XPathException {
460
        if (target == Object.class || target.isAssignableFrom(DateValue.class)) {
1✔
461
            return (T) this;
1✔
462
        } else if (target.isAssignableFrom(XMLGregorianCalendar.class)) {
1✔
463
            return (T) calendar.clone();
1✔
464
        } else if (target.isAssignableFrom(GregorianCalendar.class)) {
1✔
465
            return (T) calendar.toGregorianCalendar();
1✔
466
        } else if (Date.class.equals(target)) {
1✔
467
            return (T) calendar.toGregorianCalendar().getTime();
1✔
468
        } else if (Instant.class.equals(target)) {
1!
469
            return (T)calendar.toGregorianCalendar().toInstant();
1✔
470
        }
471

472
        throw new XPathException(getExpression(), "cannot convert value of type " + Type.getTypeName(getType()) + " to Java object of type " + target.getName());
×
473
    }
474

475
    /* (non-Javadoc)
476
     * @see java.lang.Comparable#compareTo(java.lang.Object)
477
     */
478
    public int compareTo(Object o) {
479
        if (o instanceof AbstractDateTimeValue dt) {
1!
480
            return calendar.compare(dt.calendar);
1✔
481
        }
482

483
        final AtomicValue other = (AtomicValue) o;
×
484
        if (Type.subTypeOf(other.getType(), Type.DATE_TIME))
×
485
            try {
486
                //TODO : find something that will consume less resources
487
                return calendar.compare(TimeUtils.getInstance().newXMLGregorianCalendar(other.getStringValue()));
×
488
            } catch (final XPathException e) {
×
489
                LOG.error("Failed to get string value of '{}'", other, e);
×
490
                //Why not ?
491
                return Constants.SUPERIOR;
×
492
            }
493
        else {
494
            return getType() > other.getType() ? Constants.SUPERIOR : Constants.INFERIOR;
×
495
        }
496
    }
497

498
    public boolean equals(Object obj) {
499
        if (obj instanceof AbstractDateTimeValue dt) {
1!
500
            return calendar.equals(dt.calendar);
1✔
501
        }
502

503
        return false;
×
504
    }
505

506
    public int hashCode() {
507
        return calendar.hashCode();
1✔
508
    }
509

510
    public int hashCodeWithTimeZone() {
511
        if (hasTimezone()) {
×
512
            return hashCode();
×
513
        }
514

515
        final TimeZone implicitTimeZone = TimeZone.getDefault();
×
516
        if (implicitTimeZone.inDaylightTime(new Date())) {
×
517
            implicitTimeZone.setRawOffset(implicitTimeZone.getRawOffset() + implicitTimeZone.getDSTSavings());
×
518
        }
519

520
        final XMLGregorianCalendar xgc = (XMLGregorianCalendar) calendar.clone();
×
521
        xgc.setTimezone((implicitTimeZone.getRawOffset() / 1000) / 60);
×
522

523
        return xgc.hashCode();
×
524
    }
525

526
    /**
527
     * Get's the numeric day of the week.
528
     *
529
     * Note that numbering starts from {@link Calendar#SUNDAY} which
530
     * is day 1.
531
     *
532
     * @return the day of the week.
533
     */
534
    public int getDayOfWeek() {
535
        return calendar.toGregorianCalendar().get(Calendar.DAY_OF_WEEK);
1✔
536
    }
537

538
    public int getDayWithinYear() {
539
        final int j = getJulianDayNumber(calendar.getYear(), calendar.getMonth(), calendar.getDay());
×
540
        final int k = getJulianDayNumber(calendar.getYear(), 1, 1);
×
541
        return j - k + 1;
×
542
    }
543

544
    public int getWeekWithinYear() {
545
        return calendar.toGregorianCalendar().get(Calendar.WEEK_OF_YEAR);
×
546
    }
547

548
    public int getWeekWithinMonth() {
549
        return calendar.toGregorianCalendar().get(Calendar.WEEK_OF_MONTH);
×
550
    }
551

552
    //copy from org.apache.xerces.jaxp.datatype.XMLGregorianCalendarImpl
553
    private XMLGregorianCalendar parse(String lexicalRepresentation) {
554
        // compute format string for this lexical representation.
555
        String format = null;
1✔
556
        final String lexRep = lexicalRepresentation;
1✔
557
        final int NOT_FOUND = -1;
1✔
558
        int lexRepLength = lexRep.length();
1✔
559

560
        // current parser needs a format string,
561
        // use following heuristics to figure out what xml schema date/time
562
        // datatype this lexical string could represent.
563
        if (lexRep.indexOf('T') != NOT_FOUND) {
1✔
564
            // found Date Time separater, must be xsd:DateTime
565
            format = "%Y-%M-%DT%h:%m:%s" + "%z";
1✔
566
        } else if (lexRepLength >= 3 && lexRep.charAt(2) == ':') {
1!
567
            // found ":", must be xsd:Time
568
            format = "%h:%m:%s" + "%z";
1✔
569
        } else if (lexRep.startsWith("--")) {
1✔
570
            // check for GDay || GMonth || GMonthDay
571
            if (lexRepLength >= 3 && lexRep.charAt(2) == '-') {
1!
572
                // GDAY
573
                // Fix 4971612: invalid SCCS macro substitution in data string
574
                format = "---%D" + "%z";
×
575
            } else if (lexRepLength == 4 || (lexRepLength >= 6 && (lexRep.charAt(4) == '+' || (lexRep.charAt(4) == '-' && (lexRep.charAt(5) == '-' || lexRepLength == 10))))) {
1!
576
                // GMonth
577
                // Fix 4971612: invalid SCCS macro substitution in data string
578
                format = "--%M--%Z";
1✔
579
                final Parser p = new Parser(format, lexRep);
1✔
580
                try {
581
                    final XMLGregorianCalendar c = p.parse();
×
582
                    // check for validity
583
                    if (!c.isValid()) {
×
584
                        throw new IllegalArgumentException(
×
585
                                DatatypeMessageFormatter.formatMessage(null, "InvalidXGCRepresentation", new Object[]{lexicalRepresentation})
×
586
                                //"\"" + lexicalRepresentation + "\" is not a valid representation of an XML Gregorian Calendar value."
587
                        );
588
                    }
589
                    return c;
×
590
                } catch (final IllegalArgumentException e) {
1✔
591
                    format = "--%M%z";
1✔
592
                }
593
            } else {
1✔
594
                // GMonthDay or invalid lexicalRepresentation
595
                format = "--%M-%D" + "%z";
×
596
            }
597
        } else {
×
598
            // check for Date || GYear | GYearMonth
599
            int countSeparator = 0;
1✔
600

601
            // start at index 1 to skip potential negative sign for year.
602

603

604
            final int timezoneOffset = lexRep.indexOf(':');
1✔
605
            if (timezoneOffset != NOT_FOUND) {
1✔
606

607
                // found timezone, strip it off for distinguishing
608
                // between Date, GYear and GYearMonth so possible
609
                // negative sign in timezone is not mistaken as
610
                // a separator.
611
                lexRepLength -= 6;
1✔
612
            }
613

614
            for (int i = 1; i < lexRepLength; i++) {
1✔
615
                if (lexRep.charAt(i) == '-') {
1✔
616
                    countSeparator++;
1✔
617
                }
618
            }
619
            if (countSeparator == 0) {
1!
620
                // GYear
621
                format = "%Y" + "%z";
×
622
            } else if (countSeparator == 1) {
1!
623
                // GYearMonth
624
                format = "%Y-%M" + "%z";
×
625
            } else {
×
626
                // Date or invalid lexicalRepresentation
627
                // Fix 4971612: invalid SCCS macro substitution in data string
628
                format = "%Y-%M-%D" + "%z";
1✔
629
            }
630
        }
631
        final Parser p = new Parser(format, lexRep);
1✔
632
        final XMLGregorianCalendar c = p.parse();
1✔
633

634
        // check for validity
635
        if (!c.isValid()) {
1!
636
            throw new IllegalArgumentException(
×
637
                    DatatypeMessageFormatter.formatMessage(null, "InvalidXGCRepresentation", new Object[]{lexicalRepresentation})
×
638
                    //"\"" + lexicalRepresentation + "\" is not a valid representation of an XML Gregorian Calendar value."
639
            );
640
        }
641
        return c;
1✔
642
    }
643

644
    private final class Parser {
645
        private final String format;
646
        private final String value;
647

648
        private final int flen;
649
        private final int vlen;
650

651
        private int fidx;
652
        private int vidx;
653

654
        private BigInteger year = null;
1✔
655
        private int month = DatatypeConstants.FIELD_UNDEFINED;
1✔
656
        private int day = DatatypeConstants.FIELD_UNDEFINED;
1✔
657

658
        private int timezone = DatatypeConstants.FIELD_UNDEFINED;
1✔
659

660
        private int hour = DatatypeConstants.FIELD_UNDEFINED;
1✔
661
        private int minute = DatatypeConstants.FIELD_UNDEFINED;
1✔
662
        private int second = DatatypeConstants.FIELD_UNDEFINED;
1✔
663

664
        private BigDecimal fractionalSecond = null;
1✔
665

666
        private Parser(String format, String value) {
1✔
667
            this.format = format;
1✔
668
            this.value = value;
1✔
669
            this.flen = format.length();
1✔
670
            this.vlen = value.length();
1✔
671
        }
1✔
672

673
        /**
674
         * Parse a formated <code>String</code> into an <code>XMLGregorianCalendar</code>.
675
         *
676
         * If <code>String</code> is not formated as a legal <code>XMLGregorianCalendar</code> value,
677
         * an <code>IllegalArgumentException</code> is thrown.
678
         *
679
         * @throws IllegalArgumentException If <code>String</code> is not formated as a legal <code>XMLGregorianCalendar</code> value.
680
         */
681
        public XMLGregorianCalendar parse() throws IllegalArgumentException {
682
            char vch;
683
            while (fidx < flen) {
1✔
684
                final char fch = format.charAt(fidx++);
1✔
685

686
                if (fch != '%') { // not a meta character
1✔
687
                    skip(fch);
1✔
688
                    continue;
1✔
689
                }
690

691
                // seen meta character. we don't do error check against the format
692
                switch (format.charAt(fidx++)) {
1!
693
                    case 'Y': // year
694
                        parseYear();
1✔
695
                        break;
1✔
696

697
                    case 'M': // month
698
                        month = parseInt(2, 2);
1✔
699
                        break;
1✔
700

701
                    case 'D': // days
702
                        day = parseInt(2, 2);
1✔
703
                        break;
1✔
704

705
                    case 'h': // hours
706
                        hour = parseInt(2, 2);
1✔
707
                        break;
1✔
708

709
                    case 'm': // minutes
710
                        minute = parseInt(2, 2);
1✔
711
                        break;
1✔
712

713
                    case 's': // parse seconds.
714
                        second = parseInt(2, 2);
1✔
715

716
                        if (peek() == '.') {
1✔
717
                            fractionalSecond = parseBigDecimal();
1✔
718
                        }
719
                        break;
1✔
720

721
                    case 'z': // time zone. missing, 'Z', or [+-]nn:nn
722
                        vch = peek();
1✔
723
                        if (vch == 'Z') {
1✔
724
                            vidx++;
1✔
725
                            timezone = 0;
1✔
726
                        } else if (vch == '+' || vch == '-') {
1✔
727
                            vidx++;
1✔
728
                            final int h = parseInt(2, 2);
1✔
729
                            skip(':');
1✔
730
                            final int m = parseInt(2, 2);
1✔
731

732
                            if (m >= 60 || m < 0)
1!
733
                                throw new IllegalArgumentException(
×
734
                                        DatatypeMessageFormatter.formatMessage(null, "InvalidFieldValue", new Object[]{m, "timezone minutes"})
×
735
                                );
736

737
                            timezone = (h * 60 + m) * (vch == '+' ? 1 : -1);
1✔
738
                        }
739
                        break;
1✔
740

741
                    case 'Z': // time zone. 'Z', or [+-]nn:nn
742
                        vch = peek();
×
743
                        if (vch == 'Z') {
×
744
                            vidx++;
×
745
                            timezone = 0;
×
746
                        } else if (vch == '+' || vch == '-') {
×
747
                            vidx++;
×
748
                            final int h = parseInt(2, 2);
×
749
                            skip(':');
×
750
                            final int m = parseInt(2, 2);
×
751

752
                            if (m >= 60 || m < 0)
×
753
                                throw new IllegalArgumentException(
×
754
                                        DatatypeMessageFormatter.formatMessage(null, "InvalidFieldValue", new Object[]{m, "timezone minutes"})
×
755
                                );
756

757
                            timezone = (h * 60 + m) * (vch == '+' ? 1 : -1);
×
758
                        } else {
×
759
                            throw new IllegalArgumentException(
×
760
                                    DatatypeMessageFormatter.formatMessage(null, "InvalidFieldValue", new Object[]{"do not defined", "timezone"})
×
761
                            );
762
                        }
763
                        break;
764

765
                    default:
766
                        // illegal meta character. impossible.
767
                        throw new InternalError();
×
768
                }
769
            }
770

771
            if (vidx != vlen) {
1!
772
                // some tokens are left in the input
773
                throw new IllegalArgumentException(value); //,vidx);
×
774
            }
775

776
            if (hour == 24 && minute == 0 && second == 0) {
1!
777
                if (getType() == Type.TIME) {
1✔
778
                    hour = 0;
1✔
779
                }
780
            }
781

782
            return TimeUtils.getInstance().getFactory()
1✔
783
                    .newXMLGregorianCalendar(year, month, day, hour, minute, second, fractionalSecond, timezone);
1✔
784
        }
785

786
        private char peek() throws IllegalArgumentException {
787
            if (vidx == vlen) {
1✔
788
                return (char) -1;
1✔
789
            }
790
            return value.charAt(vidx);
1✔
791
        }
792

793
        private char read() throws IllegalArgumentException {
794
            if (vidx == vlen) {
1!
795
                throw new IllegalArgumentException(value); //,vidx);
×
796
            }
797
            return value.charAt(vidx++);
1✔
798
        }
799

800
        private void skip(char ch) throws IllegalArgumentException {
801
            if (read() != ch) {
1✔
802
                throw new IllegalArgumentException(value); //,vidx-1);
1✔
803
            }
804
        }
1✔
805

806
        private void parseYear()
807
                throws IllegalArgumentException {
808
            final int vstart = vidx;
1✔
809
            int sign = 0;
1✔
810

811
            // skip leading negative, if it exists
812
            if (peek() == '-') {
1!
813
                vidx++;
×
814
                sign = 1;
×
815
            }
816
            while (isDigit(peek())) {
1✔
817
                vidx++;
1✔
818
            }
819
            final int digits = vidx - vstart - sign;
1✔
820
            if (digits < 4) {
1✔
821
                // we are expecting more digits
822
                throw new IllegalArgumentException(value); //,vidx);
1✔
823
            }
824
            final String yearString = value.substring(vstart, vidx);
1✔
825
//            if (digits < 10) {
826
//                    year = Integer.parseInt(yearString);
827
//            }
828
//            else {
829
            year = new BigInteger(yearString);
1✔
830
//            }
831
        }
1✔
832

833
        private int parseInt(int minDigits, int maxDigits)
834
                throws IllegalArgumentException {
835
            final int vstart = vidx;
1✔
836
            while (isDigit(peek()) && (vidx - vstart) < maxDigits) {
1!
837
                vidx++;
1✔
838
            }
839
            if ((vidx - vstart) < minDigits) {
1!
840
                // we are expecting more digits
841
                throw new IllegalArgumentException(value); //,vidx);
×
842
            }
843

844
            // NumberFormatException is IllegalArgumentException
845
            //           try {
846
            return Integer.parseInt(value.substring(vstart, vidx));
1✔
847
            //            } catch( NumberFormatException e ) {
848
            //                // if the value is too long for int, NumberFormatException is thrown
849
            //                throw new IllegalArgumentException(value,vstart);
850
            //            }
851
        }
852

853
        private BigDecimal parseBigDecimal()
854
                throws IllegalArgumentException {
855
            final int vstart = vidx;
1✔
856

857
            if (peek() == '.') {
1!
858
                vidx++;
1✔
859
            } else {
1✔
860
                throw new IllegalArgumentException(value);
×
861
            }
862
            while (isDigit(peek())) {
1✔
863
                vidx++;
1✔
864
            }
865
            return new BigDecimal(value.substring(vstart, vidx));
1✔
866
        }
867
    }
868

869
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc