Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
H
histream
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
Operations
Operations
Incidents
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Raphael
histream
Commits
de135289
Commit
de135289
authored
Aug 22, 2017
by
R.W.Majeed
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
rewrote parsing to allow time zone offset at any time accuracy
parent
758fb772
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
154 additions
and
72 deletions
+154
-72
histream-core/src/main/java/de/sekmi/histream/DateTimeAccuracy.java
...ore/src/main/java/de/sekmi/histream/DateTimeAccuracy.java
+96
-69
histream-core/src/test/java/de/sekmi/histream/TestDateTimeAccuracy.java
...src/test/java/de/sekmi/histream/TestDateTimeAccuracy.java
+58
-3
No files found.
histream-core/src/main/java/de/sekmi/histream/DateTimeAccuracy.java
View file @
de135289
...
...
@@ -3,6 +3,7 @@ package de.sekmi.histream;
import
java.text.ParseException
;
import
java.text.ParsePosition
;
import
java.time.DateTimeException
;
import
java.time.Instant
;
/*
* #%L
...
...
@@ -26,6 +27,7 @@ import java.time.DateTimeException;
import
java.time.LocalDateTime
;
import
java.time.OffsetTime
;
import
java.time.ZoneId
;
import
java.time.ZoneOffset
;
import
java.time.ZonedDateTime
;
...
...
@@ -37,6 +39,7 @@ import java.time.temporal.Temporal;
import
java.time.temporal.TemporalAccessor
;
import
java.time.temporal.TemporalField
;
import
java.time.temporal.TemporalUnit
;
import
java.time.temporal.UnsupportedTemporalTypeException
;
import
java.util.Date
;
import
java.util.Objects
;
...
...
@@ -47,11 +50,14 @@ import de.sekmi.histream.xml.DateTimeAccuracyAdapter;
/**
* Local date and time with specified accuracy. Maximum resolution is seconds.
* For supported accuracy, see {@link #setAccuracy(ChronoUnit)}.
* @author R
aphael
* @author R
.W.Majeed
*
*/
@XmlJavaTypeAdapter
(
DateTimeAccuracyAdapter
.
class
)
public
class
DateTimeAccuracy
implements
Temporal
,
Comparable
<
DateTimeAccuracy
>
{
static
final
String
PARTIAL_FORMATTER_PATTERN
=
"u[-M[-d['T'H[:m[:s[.S]]][X]]]]"
;
static
final
DateTimeFormatter
PARTIAL_FORMATTER
=
DateTimeFormatter
.
ofPattern
(
PARTIAL_FORMATTER_PATTERN
);
// TODO why not use instant, since we always calculate UTC? or Offset/ZonedDateTime?
private
LocalDateTime
dateTime
;
private
ChronoUnit
accuracy
;
...
...
@@ -97,7 +103,18 @@ public class DateTimeAccuracy implements Temporal, Comparable<DateTimeAccuracy>
dateTime
.
truncatedTo
(
accuracy
);
}
// Temporal interface behaves like undelaying dateTime
/**
* Convert the partial date time to an instant.
* Will return the minimum instant for the given accuracy.
* E.g. accuracy of YEAR will return the the first second in the given year.
* @return minimum instant within the given accuracy
*/
public
Instant
toInstantMin
(){
return
dateTime
.
toInstant
(
ZoneOffset
.
UTC
);
}
// TODO toInstantMax() (increase field at accuracy and subtract one millisecond)
// Temporal interface behaves like underlaying dateTime
@Override
public
long
getLong
(
TemporalField
arg0
)
{
return
dateTime
.
getLong
(
arg0
);}
@Override
...
...
@@ -161,14 +178,16 @@ public class DateTimeAccuracy implements Temporal, Comparable<DateTimeAccuracy>
* @param digits digits to add
*/
private
static
void
appendWithZeroPrefix
(
StringBuilder
builder
,
TemporalAccessor
date
,
TemporalField
field
,
int
digits
){
int
v
=
date
.
get
(
field
);
padZeros
(
builder
,
date
.
get
(
field
),
digits
);
}
private
static
void
padZeros
(
StringBuilder
builder
,
int
value
,
int
digits
){
int
pow
=
1
;
for
(
int
i
=
1
;
i
<
digits
;
i
++
)
pow
*=
10
;
while
(
v
<
pow
&&
pow
>
1
){
while
(
v
alue
<
pow
&&
pow
>
1
){
builder
.
append
(
'0'
);
pow
/=
10
;
}
builder
.
append
(
v
);
builder
.
append
(
v
alue
);
}
/**
* Convert the date to a partial ISO 8601 date time string.
...
...
@@ -195,8 +214,9 @@ public class DateTimeAccuracy implements Temporal, Comparable<DateTimeAccuracy>
TemporalAccessor
dt
;
if
(
tz
!=
null
){
// use timezone information
dt
=
dateTime
.
atZone
(
tz
);
// use timezone information.
// Assume that dateTime is given in UTC. For output convert to destination timezone.
dt
=
dateTime
.
atOffset
(
ZoneOffset
.
UTC
).
atZoneSameInstant
(
tz
);
}
else
{
// no zone info, output will not have offset
dt
=
dateTime
;
...
...
@@ -214,84 +234,91 @@ public class DateTimeAccuracy implements Temporal, Comparable<DateTimeAccuracy>
if
(
tz
!=
null
&&
i
>=
3
){
// hours present
// add zone offset
String
of
=
((
ZonedDateTime
)
dt
).
getOffset
().
normalized
().
toString
();
b
.
append
(
of
);
int
os
=
((
ZonedDateTime
)
dt
).
getOffset
().
getTotalSeconds
();
if
(
os
==
0
){
// output Z
b
.
append
(
'Z'
);
}
else
{
// append sign and four characters
if
(
os
<
0
){
b
.
append
(
'-'
);
}
else
{
b
.
append
(
'+'
);
}
// hours
int
ox
=
os
/
3600
;
os
=
os
%
3600
;
padZeros
(
b
,
ox
,
2
);
// minutes
ox
=
os
/
60
;
padZeros
(
b
,
ox
,
2
);
// ignore seconds, not part of ISO
}
}
return
b
.
toString
();
}
/**
* Parses a partial ISO 8601 date time string.
* [-]CCYY-MM-DDThh:mm:ss[Z|(+|-)hh:mm]
* [-]CCYY-MM-DDThh:mm:ss[Z|(+|-)hhmm]
* <p>
* At least the year must be specified. All other fields can be left out.
*
* @param str ISO 8601 string
* @return date time with accuracy as derived from parse
* @throws ParseException for unparsable string
* @throws IllegalArgumentException unparsable string (old unchecked exception)
*/
public
static
DateTimeAccuracy
parsePartialIso8601
(
String
str
)
throws
ParseException
{
if
(
str
.
length
()
<
4
)
throw
new
ParseException
(
"Need at least 4 characters for year: "
+
str
,
str
.
length
());
// parse year
int
year
=
Integer
.
parseInt
(
str
.
substring
(
0
,
4
));
if
(
str
.
length
()
==
4
){
// specified to accuracy of years
return
new
DateTimeAccuracy
(
year
);
}
else
if
(
str
.
length
()
<
7
||
str
.
charAt
(
4
)
!=
'-'
){
throw
new
ParseException
(
"Expected YYYY-MM"
,
Integer
.
min
(
4
,
str
.
length
()));
}
// parse month
int
month
=
Integer
.
parseInt
(
str
.
substring
(
5
,
7
));
if
(
str
.
length
()
==
7
){
// specified to accuracy of months
return
new
DateTimeAccuracy
(
year
,
month
);
}
else
if
(
str
.
length
()
<
10
||
str
.
charAt
(
7
)
!=
'-'
){
throw
new
ParseException
(
"Expected YYYY-MM-DD"
,
Integer
.
min
(
7
,
str
.
length
()));
}
// parse day
int
day
=
Integer
.
parseInt
(
str
.
substring
(
8
,
10
));
if
(
str
.
length
()
==
10
){
// specified to accuracy of days
return
new
DateTimeAccuracy
(
year
,
month
,
day
);
}
else
if
(
str
.
length
()
<
13
||
str
.
charAt
(
10
)
!=
'T'
){
throw
new
ParseException
(
"Expected yyyy-mm-ddThh"
,
Integer
.
min
(
10
,
str
.
length
()));
}
// parse hours
int
hours
=
Integer
.
parseInt
(
str
.
substring
(
11
,
13
));
if
(
str
.
length
()
==
13
){
// specified to accuracy of hours
return
new
DateTimeAccuracy
(
year
,
month
,
day
,
hours
);
}
else
if
(
str
.
length
()
<
16
||
str
.
charAt
(
13
)
!=
':'
){
throw
new
ParseException
(
"Expected yyyy-mm-ddThh:mm"
,
Integer
.
min
(
13
,
str
.
length
()));
}
// parse minutes
int
mins
=
Integer
.
parseInt
(
str
.
substring
(
14
,
16
));
if
(
str
.
length
()
==
16
){
// specified to accuracy of minutes
return
new
DateTimeAccuracy
(
year
,
month
,
day
,
hours
,
mins
);
}
else
if
(
str
.
length
()
<
19
||
str
.
charAt
(
16
)
!=
':'
){
throw
new
ParseException
(
"Expected yyyy-mm-ddThh:mm:ss"
,
Integer
.
min
(
16
,
str
.
length
()));
ParsePosition
pos
=
new
ParsePosition
(
0
);
TemporalAccessor
a
=
PARTIAL_FORMATTER
.
parseUnresolved
(
str
,
pos
);
// first check that everything was parsed
if
(
pos
.
getErrorIndex
()
!=
-
1
){
throw
new
ParseException
(
"Parse error at position "
+
pos
.
getErrorIndex
(),
pos
.
getErrorIndex
());
}
else
if
(
pos
.
getIndex
()
!=
str
.
length
()
){
throw
new
ParseException
(
"Unparsed text found at index "
+
pos
.
getIndex
()+
": "
+
str
.
substring
(
pos
.
getIndex
()),
pos
.
getIndex
());
}
// parse seconds
int
secs
=
Integer
.
parseInt
(
str
.
substring
(
17
,
19
));
if
(
str
.
length
()
==
19
||
(
str
.
length
()
==
20
&&
str
.
charAt
(
19
)
==
'Z'
)
){
// specified to accuracy of seconds
return
new
DateTimeAccuracy
(
year
,
month
,
day
,
hours
,
mins
,
secs
);
}
else
if
(
str
.
length
()
<
25
||
!(
str
.
charAt
(
19
)
!=
'+'
||
str
.
charAt
(
19
)
!=
'-'
)
){
throw
new
ParseException
(
"Expected yyyy-mm-ddThh:mm:ss[Z|+oo:oo]"
,
19
);
}
else
if
(
str
.
length
()
!=
25
||
str
.
charAt
(
22
)
!=
':'
){
// handles longer input and missing : in offset
throw
new
ParseException
(
"Expected yyyy-mm-ddThh:mm:ss[Z|+oo:oo]"
,
22
);
// everything parsed without error
// now check for accuracy
ChronoUnit
accuracy
;
LocalDateTime
dateTime
;
if
(
a
.
isSupported
(
ChronoField
.
NANO_OF_SECOND
)
){
// maximum accuracy of nanoseconds
// not supported yet, truncate to seconds
accuracy
=
ChronoUnit
.
NANOS
;
dateTime
=
LocalDateTime
.
from
(
a
);
}
else
if
(
a
.
isSupported
(
ChronoField
.
SECOND_OF_MINUTE
)
){
accuracy
=
ChronoUnit
.
SECONDS
;
dateTime
=
LocalDateTime
.
of
(
a
.
get
(
ChronoField
.
YEAR
),
a
.
get
(
ChronoField
.
MONTH_OF_YEAR
),
a
.
get
(
ChronoField
.
DAY_OF_MONTH
),
a
.
get
(
ChronoField
.
HOUR_OF_DAY
),
a
.
get
(
ChronoField
.
MINUTE_OF_HOUR
),
a
.
get
(
ChronoField
.
SECOND_OF_MINUTE
));
}
else
if
(
a
.
isSupported
(
ChronoField
.
MINUTE_OF_HOUR
)
){
accuracy
=
ChronoUnit
.
MINUTES
;
dateTime
=
LocalDateTime
.
of
(
a
.
get
(
ChronoField
.
YEAR
),
a
.
get
(
ChronoField
.
MONTH_OF_YEAR
),
a
.
get
(
ChronoField
.
DAY_OF_MONTH
),
a
.
get
(
ChronoField
.
HOUR_OF_DAY
),
a
.
get
(
ChronoField
.
MINUTE_OF_HOUR
));
}
else
if
(
a
.
isSupported
(
ChronoField
.
HOUR_OF_DAY
)
){
accuracy
=
ChronoUnit
.
HOURS
;
dateTime
=
LocalDateTime
.
of
(
a
.
get
(
ChronoField
.
YEAR
),
a
.
get
(
ChronoField
.
MONTH_OF_YEAR
),
a
.
get
(
ChronoField
.
DAY_OF_MONTH
),
a
.
get
(
ChronoField
.
HOUR_OF_DAY
),
0
);
}
else
if
(
a
.
isSupported
(
ChronoField
.
DAY_OF_MONTH
)
){
accuracy
=
ChronoUnit
.
DAYS
;
dateTime
=
LocalDateTime
.
of
(
a
.
get
(
ChronoField
.
YEAR
),
a
.
get
(
ChronoField
.
MONTH_OF_YEAR
),
a
.
get
(
ChronoField
.
DAY_OF_MONTH
),
0
,
0
);
}
else
if
(
a
.
isSupported
(
ChronoField
.
MONTH_OF_YEAR
)
){
dateTime
=
LocalDateTime
.
of
(
a
.
get
(
ChronoField
.
YEAR
),
a
.
get
(
ChronoField
.
MONTH_OF_YEAR
),
1
,
0
,
0
);
accuracy
=
ChronoUnit
.
MONTHS
;
}
else
{
DateTimeAccuracy
me
=
new
DateTimeAccuracy
(
year
,
month
,
day
,
hours
,
mins
,
secs
);
// parse time zone
ZoneOffset
of
=
ZoneOffset
.
ofHoursMinutes
(
Integer
.
parseInt
(
str
.
substring
(
20
,
22
)),
Integer
.
parseInt
(
str
.
substring
(
24
,
25
))
);
// format requires at least year
accuracy
=
ChronoUnit
.
YEARS
;
dateTime
=
LocalDateTime
.
of
(
a
.
get
(
ChronoField
.
YEAR
),
1
,
1
,
0
,
0
);
}
// check for zone offset
ZoneOffset
off
=
null
;
if
(
a
.
isSupported
(
ChronoField
.
OFFSET_SECONDS
)
){
off
=
ZoneOffset
.
ofTotalSeconds
(
a
.
get
(
ChronoField
.
OFFSET_SECONDS
));
// adjust to UTC
// TODO unit test for this behavior
me
.
dateTime
=
me
.
dateTime
.
atOffset
(
of
).
withOffsetSameInstant
(
ZoneOffset
.
UTC
).
toLocalDateTime
();
return
me
;
dateTime
=
dateTime
.
atOffset
(
off
).
withOffsetSameInstant
(
ZoneOffset
.
UTC
).
toLocalDateTime
();
}
// unparsed data (longer input) will be handled above
//throw new ParseException("Unparsed data at index 26", 26);
DateTimeAccuracy
me
=
new
DateTimeAccuracy
(
dateTime
);
me
.
accuracy
=
accuracy
;
return
me
;
}
/**
...
...
histream-core/src/test/java/de/sekmi/histream/TestDateTimeAccuracy.java
View file @
de135289
...
...
@@ -7,8 +7,11 @@ import java.time.format.DateTimeParseException;
import
java.time.format.ResolverStyle
;
import
java.time.temporal.ChronoField
;
import
java.time.temporal.ChronoUnit
;
import
java.time.temporal.TemporalAccessor
;
import
java.time.temporal.UnsupportedTemporalTypeException
;
import
org.junit.Assert
;
import
static
org
.
junit
.
Assert
.*;
import
org.junit.Test
;
public
class
TestDateTimeAccuracy
{
...
...
@@ -34,10 +37,62 @@ public class TestDateTimeAccuracy {
a
=
DateTimeAccuracy
.
parse
(
formatter
,
"01.02.2003 13"
);
Assert
.
assertEquals
(
ChronoUnit
.
HOURS
,
a
.
getAccuracy
());
Assert
.
assertEquals
(
"2003-02-01T13"
,
a
.
toPartialIso8601
(
null
));
Assert
.
assertEquals
(
"2003-02-01T
13+08:
00"
,
a
.
toPartialIso8601
(
tz
));
Assert
.
assertEquals
(
"2003-02-01T
21+08
00"
,
a
.
toPartialIso8601
(
tz
));
}
@Test
public
void
verifyDateTimeFormatter
(){
TemporalAccessor
a
;
DateTimeFormatter
f
=
DateTimeFormatter
.
ISO_DATE_TIME
;
// zone offset missing, field should be not available
a
=
f
.
parse
(
"2001-02-03T04:05:06"
);
Assert
.
assertFalse
(
a
.
isSupported
(
ChronoField
.
OFFSET_SECONDS
));
try
{
a
.
get
(
ChronoField
.
OFFSET_SECONDS
);
Assert
.
fail
(
"Expected exception not thrown"
);
}
catch
(
UnsupportedTemporalTypeException
e
){
// expected outcome
}
// zero zone offset, field should be available
a
=
f
.
parse
(
"2001-02-03T04:05:06Z"
);
Assert
.
assertEquals
(
0
,
a
.
get
(
ChronoField
.
OFFSET_SECONDS
));
a
=
f
.
parse
(
"2001-02-03T04:05:06+00:00"
);
Assert
.
assertEquals
(
0
,
a
.
get
(
ChronoField
.
OFFSET_SECONDS
));
a
=
f
.
parse
(
"2001-02-03T04:05"
);
// seconds can be omitted
// test the partial timestamp formatter
f
=
DateTimeFormatter
.
ofPattern
(
"u[-M[-d['T'H[:m[:s[.S]]][X]]]]"
);
}
@Test
public
void
verifyParsingIncompleteIsoTimestamp
()
throws
ParseException
{
DateTimeAccuracy
a
;
a
=
DateTimeAccuracy
.
parsePartialIso8601
(
"2001"
);
assertEquals
(
ChronoUnit
.
YEARS
,
a
.
getAccuracy
());
a
=
DateTimeAccuracy
.
parsePartialIso8601
(
"2001-02"
);
assertEquals
(
ChronoUnit
.
MONTHS
,
a
.
getAccuracy
());
a
=
DateTimeAccuracy
.
parsePartialIso8601
(
"2001-02-03"
);
assertEquals
(
ChronoUnit
.
DAYS
,
a
.
getAccuracy
());
a
=
DateTimeAccuracy
.
parsePartialIso8601
(
"2001-02-03T04"
);
assertEquals
(
ChronoUnit
.
HOURS
,
a
.
getAccuracy
());
a
=
DateTimeAccuracy
.
parsePartialIso8601
(
"2001-02-03T04:05"
);
assertEquals
(
ChronoUnit
.
MINUTES
,
a
.
getAccuracy
());
a
=
DateTimeAccuracy
.
parsePartialIso8601
(
"2001-02-03T04:05:06"
);
assertEquals
(
ChronoUnit
.
SECONDS
,
a
.
getAccuracy
());
// verify zone offset
// for second accuracy
a
=
DateTimeAccuracy
.
parsePartialIso8601
(
"2001-02-03T04:05:06+0800"
);
assertEquals
(
ChronoUnit
.
SECONDS
,
a
.
getAccuracy
());
// zone offset calculation
assertEquals
(
DateTimeAccuracy
.
parsePartialIso8601
(
"2001-02-02T20:05:06Z"
),
a
);
a
=
DateTimeAccuracy
.
parsePartialIso8601
(
"2001-02-03T04+0800"
);
assertEquals
(
ChronoUnit
.
HOURS
,
a
.
getAccuracy
());
a
=
DateTimeAccuracy
.
parsePartialIso8601
(
"2001-02-03T04Z"
);
assertEquals
(
ChronoUnit
.
HOURS
,
a
.
getAccuracy
());
a
=
DateTimeAccuracy
.
parsePartialIso8601
(
"2001-02-03T04:05+0800"
);
assertEquals
(
ChronoUnit
.
MINUTES
,
a
.
getAccuracy
());
}
@Test
public
void
testFormatExceedsText
(){
DateTimeFormatter
formatter
=
DateTimeFormatter
.
ofPattern
(
"d.M.u[ H[:m[:s]]]"
);
...
...
@@ -63,7 +118,7 @@ public class TestDateTimeAccuracy {
}
// TODO test more aspects of zone offset parsing
DateTimeAccuracy
.
parsePartialIso8601
(
"2003-02-01T04:05:06Z"
);
DateTimeAccuracy
a
=
DateTimeAccuracy
.
parsePartialIso8601
(
"2003-02-01T04:05:06+01
:
00"
);
DateTimeAccuracy
a
=
DateTimeAccuracy
.
parsePartialIso8601
(
"2003-02-01T04:05:06+0100"
);
// make sure the date is adjusted to UTC
Assert
.
assertEquals
(
3
,
a
.
get
(
ChronoField
.
HOUR_OF_DAY
));
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment