• 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

89.76
/exist-core/src/main/java/org/exist/xquery/functions/fn/FunParseIetfDate.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.functions.fn;
50

51
import org.exist.dom.QName;
52
import org.exist.xquery.*;
53
import org.exist.xquery.value.*;
54

55
import javax.xml.datatype.DatatypeConstants;
56
import javax.xml.datatype.XMLGregorianCalendar;
57

58
import java.math.BigDecimal;
59
import java.math.BigInteger;
60

61
import java.util.Map;
62
import java.util.HashMap;
63
import java.util.Arrays;
64

65
import static org.exist.util.StringUtil.startsWith;
66

67
/**
68
 * Parses a string containing the date and time in IETF format,
69
 * returning the corresponding xs:dateTime value.
70
 *
71
 * @author Juri Leino (juri@existsolutions.com)
72
 */
73
public class FunParseIetfDate extends BasicFunction {
74

75
    private static FunctionParameterSequenceType IETF_DATE =
1✔
76
            new FunctionParameterSequenceType(
1✔
77
                    "value", Type.STRING, Cardinality.ZERO_OR_ONE, "The IETF-dateTime string");
1✔
78

79
    private static FunctionReturnSequenceType RETURN =
1✔
80
            new FunctionReturnSequenceType(
1✔
81
                    Type.DATE_TIME, Cardinality.ZERO_OR_ONE, "The parsed date");
1✔
82

83

84
    public final static FunctionSignature FNS_PARSE_IETF_DATE = new FunctionSignature(
1✔
85
            new QName("parse-ietf-date", Function.BUILTIN_FUNCTION_NS),
1✔
86
            "Parses a string containing the date and time in IETF format,\n" +
1✔
87
                    "returning the corresponding xs:dateTime value.",
88
            new SequenceType[]{IETF_DATE},
1✔
89
            RETURN
1✔
90
    );
1✔
91

92
    public FunParseIetfDate(XQueryContext context, FunctionSignature signature) {
93
        super(context, signature);
1✔
94
    }
1✔
95

96
    @Override
97
    public Sequence eval(Sequence[] args, Sequence contextSequence) throws XPathException {
98
        if (args[0].isEmpty()) {
1✔
99
            return Sequence.EMPTY_SEQUENCE;
1✔
100
        }
101
        final String value = args[0].getStringValue();
1✔
102
        final Parser p = new Parser(value.trim());
1✔
103

104
        try {
105
            return new DateTimeValue(this, p.parse());
1✔
106
        } catch (final IllegalArgumentException i) {
1✔
107
            throw new XPathException(this, ErrorCodes.FORG0010, "Invalid Date time " + value, i);
1✔
108
        }
109
    }
110

111
    private class Parser {
112
        private final char[] WS = {0x000A, 0x0009, 0x000D, 0x0020};
1✔
113
        private final String WS_STR = new String(WS);
1✔
114

115
        private final String[] dayNames = {
1✔
116
                "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
1✔
117
                "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"
1✔
118
        };
119

120
        private final String[] monthNames = {
1✔
121
                "Jan", "Feb", "Mar", "Apr", "May", "Jun",
1✔
122
                "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
1✔
123
        };
124

125
        private final String[] tzNames = {
1✔
126
                "UT", "UTC", "GMT", "EST", "EDT", "CST", "CDT", "MST", "MDT", "PST", "PDT"
1✔
127
        };
128

129
        private final Map<String, Integer> TZ_MAP = initMap();
1✔
130
        private final String value;
131
        private final int vlen;
132
        private int vidx;
133

134
        private BigInteger year = null;
1✔
135
        private int month = DatatypeConstants.FIELD_UNDEFINED;
1✔
136
        private int day = DatatypeConstants.FIELD_UNDEFINED;
1✔
137

138
        private int hour = DatatypeConstants.FIELD_UNDEFINED;
1✔
139
        private int minute = DatatypeConstants.FIELD_UNDEFINED;
1✔
140
        private int second = DatatypeConstants.FIELD_UNDEFINED;
1✔
141
        private BigDecimal fractionalSecond = null;
1✔
142

143
        private int timezone = DatatypeConstants.FIELD_UNDEFINED;
1✔
144

145
        private Parser(String value) {
1✔
146
            this.value = value;
1✔
147
            this.vlen = value.length();
1✔
148
        }
1✔
149

150
        private Map<String, Integer> initMap() {
151
            Map<String, Integer> result = new HashMap<>();
1✔
152
            result.put("UT", 0);
1✔
153
            result.put("UTC", 0);
1✔
154
            result.put("GMT", 0);
1✔
155
            result.put("EST", -5);
1✔
156
            result.put("EDT", -4);
1✔
157
            result.put("CST", -6);
1✔
158
            result.put("CDT", -5);
1✔
159
            result.put("MST", -7);
1✔
160
            result.put("MDT", -6);
1✔
161
            result.put("PST", -8);
1✔
162
            result.put("PDT", -7);
1✔
163
            return result;
1✔
164
        }
165

166
        /**
167
         * <p>Parse a formatted <code>String</code> into an <code>XMLGregorianCalendar</code>.</p>
168
         * <p>
169
         * <p>If <code>String</code> is not formatted as a legal <code>IETF Date</code> value,
170
         * an <code>IllegalArgumentException</code> is thrown.</p>
171
         * <pre>
172
         * input        ::=        (dayname ","? S)? ((datespec S time) | asctime)
173
         * datespec        ::=        daynum dsep monthname dsep year
174
         * dsep        ::=        S | (S? "-" S?)
175
         * daynum        ::=        digit digit?
176
         * year        ::=        digit digit (digit digit)?
177
         * digit        ::=        [0-9]
178
         * time        ::=        hours ":" minutes (":" seconds)? (S? timezone)?
179
         * hours        ::=        digit digit?
180
         * minutes        ::=        digit digit
181
         * seconds        ::=        digit digit ("." digit+)?
182
         * S ::= (x0A|x09|x0D|x20)+
183
         * </pre>
184
         *
185
         * @throws IllegalArgumentException If <code>String</code> is not formatted as a legal <code>IETF Date</code> value.
186
         */
187
        public XMLGregorianCalendar parse() throws IllegalArgumentException {
188
            dayName();
1✔
189
            dateSpec();
1✔
190
            if (vidx != vlen) {
1!
191
                throw new IllegalArgumentException(value);
×
192
            }
193
            return TimeUtils
1✔
194
                    .getInstance()
1✔
195
                    .getFactory()
1✔
196
                    .newXMLGregorianCalendar(year, month, day, hour, minute, second, fractionalSecond, timezone);
1✔
197
        }
198

199
        private void dayName() {
200
            if (startsWith(value, dayNames)) {
1✔
201
                skipTo(WS_STR);
1✔
202
                vidx++;
1✔
203
            }
204
        }
1✔
205

206
        private void dateSpec() throws IllegalArgumentException {
207
            if (isWS(peek())) {
1✔
208
                skipWS();
1✔
209
            }
210
            if (startsWith(value.substring(vidx), monthNames)) {
1✔
211
                asctime();
1✔
212
            } else {
1✔
213
                rfcDate();
1✔
214
            }
215
        }
1✔
216

217
        private void rfcDate() throws IllegalArgumentException {
218
            day();
1✔
219
            dsep();
1✔
220
            month();
1✔
221
            dsep();
1✔
222
            year();
1✔
223
            skipWS();
1✔
224
            time();
1✔
225
        }
1✔
226

227
        private void asctime() throws IllegalArgumentException {
228
            month();
1✔
229
            dsep();
1✔
230
            day();
1✔
231
            skipWS();
1✔
232
            time();
1✔
233
            skipWS();
1✔
234
            year();
1✔
235
        }
1✔
236

237
        private void year() throws IllegalArgumentException {
238
            final int vstart = vidx;
1✔
239

240
            while (isDigit(peek())) {
1✔
241
                vidx++;
1✔
242
            }
243
            final int digits = vidx - vstart;
1✔
244
            String yearString;
245
            if (digits == 2) {
1✔
246
                yearString = "19" + value.substring(vstart, vidx);
1✔
247
            } else if (digits == 4) {
1!
248
                yearString = value.substring(vstart, vidx);
1✔
249
            } else {
1✔
250
                throw new IllegalArgumentException(value);
×
251
            }
252

253
            year = new BigInteger(yearString);
1✔
254
        }
1✔
255

256
        private void month() throws IllegalArgumentException {
257
            final int vstart = vidx;
1✔
258
            vidx += 3;
1✔
259
            if (vidx >= vlen) {
1✔
260
                throw new IllegalArgumentException(value);
1✔
261
            }
262
            final String monthName = value.substring(vstart, vidx);
1✔
263
            final int idx = Arrays.asList(monthNames).indexOf(monthName);
1✔
264
            if (idx < 0) {
1!
265
                throw new IllegalArgumentException(value);
×
266
            }
267
            month = idx + 1;
1✔
268
        }
1✔
269

270
        private void day() throws IllegalArgumentException {
271
            day = parseInt(1, 2);
1✔
272
        }
1✔
273

274
        private void time() throws IllegalArgumentException {
275
            hours();
1✔
276
            minutes();
1✔
277
            seconds();
1✔
278
            skipWS();
1✔
279
            timezone();
1✔
280
        }
1✔
281

282
        private void hours() throws IllegalArgumentException {
283
            hour = parseInt(2, 2);
1✔
284
        }
1✔
285

286
        private void minutes() throws IllegalArgumentException {
287
            skip(':');
1✔
288
            minute = parseInt(2, 2);
1✔
289
            checkMinutes(minute);
1✔
290
        }
1✔
291

292
        private void seconds() throws IllegalArgumentException {
293
            if (isWS(peek())) {
1!
294
                second = 0;
×
295
                return;
×
296
            }
297
            skip(':');
1✔
298
            second = parseInt(2, 2);
1✔
299
            fractionalSecond = parseBigDecimal();
1✔
300
        }
1✔
301

302
        private void timezone() throws IllegalArgumentException {
303
            if (!startsWith(value.substring(vidx), tzNames)) {
1✔
304
                tzoffset();
1✔
305
                return;
1✔
306
            }
307
            parseTimezoneName();
1✔
308
        }
1✔
309

310
        private void parseTimezoneName() {
311
            final int vstart = vidx;
1✔
312
            while (isUpperCaseLetter(peek())) {
1✔
313
                vidx++;
1✔
314
            }
315
            final String tzName = value.substring(vstart, vidx);
1✔
316
            if (!TZ_MAP.containsKey(tzName)) {
1!
317
                throw new IllegalArgumentException(value);
×
318
            }
319
            timezone = TZ_MAP.get(tzName) * 60;
1✔
320
        }
1✔
321

322
        private void tzoffset() throws IllegalArgumentException {
323
            final char sign = peek();
1✔
324
            if (!(sign == '+' || sign == '-')) {
1!
325
                throw new IllegalArgumentException(value);
×
326
            }
327

328
            vidx++;
1✔
329
            final int h = parseInt(1, 2);
1✔
330

331
            if (peek() == ':') {
1✔
332
                skip(':');
1✔
333
            }
334

335
            int m = 0;
1✔
336
            if (isDigit(peek())) {
1!
337
                m = parseInt(2, 2);
1✔
338
            }
339
            checkMinutes(m);
1✔
340

341
            final int offset = h * 60 + m;
1✔
342
            final int factor = (sign == '+' ? 1 : -1);
1✔
343
            timezone = offset * factor;
1✔
344

345
            // cut off whitespace and optional timezone in parenthesis
346
            if (isWS(peek()) || peek() == '(') {
1!
347
                vidx = vlen;
1✔
348
            }
349
        }
1✔
350

351
        private void dsep() throws IllegalArgumentException {
352
            if (isWS(peek())) {
1✔
353
                skipWS();
1✔
354
            }
355
            if (peek() != '-') {
1✔
356
                return;
1✔
357
            }
358
            skip('-');
1✔
359
            if (isWS(peek())) {
1✔
360
                skipWS();
1✔
361
            }
362
        }
1✔
363

364
        private void skipWS() throws IllegalArgumentException {
365
            if (!isWS(peek())) {
1!
366
                throw new IllegalArgumentException(value);
×
367
            }
368

369
            while (isWS(peek())) {
1✔
370
                vidx++;
1✔
371
            }
372
        }
1✔
373

374
        private char peek() {
375
            if (vidx == vlen) {
1✔
376
                return (char) -1;
1✔
377
            }
378
            return value.charAt(vidx);
1✔
379
        }
380

381
        private char read() throws IllegalArgumentException {
382
            if (vidx == vlen) {
1!
383
                throw new IllegalArgumentException(value);
×
384
            }
385
            return value.charAt(vidx++);
1✔
386
        }
387

388
        private void skipTo(String sequence) throws IllegalArgumentException {
389
            while (sequence.indexOf(peek()) < 0) {
1✔
390
                read();
1✔
391
            }
392
        }
1✔
393

394
        private void skip(char ch) throws IllegalArgumentException {
395
            if (read() != ch) throw new IllegalArgumentException(value);
1✔
396
        }
1✔
397

398
        private int parseInt(int minDigits, int maxDigits) throws IllegalArgumentException {
399
            final int vstart = vidx;
1✔
400
            while (isDigit(peek()) && (vidx - vstart) < maxDigits) {
1✔
401
                vidx++;
1✔
402
            }
403
            if ((vidx - vstart) < minDigits) {
1✔
404
                // we are expecting more digits
405
                throw new IllegalArgumentException(value);
1✔
406
            }
407

408
            return Integer.parseInt(value.substring(vstart, vidx));
1✔
409
        }
410

411
        private BigDecimal parseBigDecimal() throws IllegalArgumentException {
412
            final int vstart = vidx;
1✔
413

414
            if (peek() == '.') {
1!
415
                vidx++;
×
416
            } else {
×
417
                return new BigDecimal("0");
1✔
418
            }
419
            while (isDigit(peek())) {
×
420
                vidx++;
×
421
            }
422
            return new BigDecimal(value.substring(vstart, vidx));
×
423
        }
424

425
        private void checkMinutes(int m) {
426
            if (m >= 60 || m < 0) {
1!
427
                throw new IllegalArgumentException(value);
×
428
            }
429
        }
1✔
430

431
        private boolean isWS(char c) {
432
            return (WS_STR.indexOf(c) >= 0);
1✔
433
        }
434

435
        private boolean isDigit(char ch) {
436
            return '0' <= ch && ch <= '9';
1✔
437
        }
438

439
        private boolean isUpperCaseLetter(char ch) {
440
            return 'A' <= ch && ch <= 'Z';
1✔
441
        }
442
    }
443
}
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

© 2026 Coveralls, Inc