From 88117f8d6e86308a4c10d66a9282da6b3e7cb757 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 12 Dec 2016 07:32:53 -0500 Subject: [PATCH] Port in changes from RI --- .../fhir/dstu3/model/BaseDateTimeType.java | 542 ++++++++++-------- .../model/BaseDateTimeTypeDstu3Test.java | 70 ++- 2 files changed, 375 insertions(+), 237 deletions(-) diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/model/BaseDateTimeType.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/model/BaseDateTimeType.java index f6c2e4ce9bd..07a7afbceea 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/model/BaseDateTimeType.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/model/BaseDateTimeType.java @@ -16,13 +16,13 @@ import ca.uhn.fhir.parser.DataFormatException; public abstract class BaseDateTimeType extends PrimitiveType { - private static final long serialVersionUID = 1L; - static final long NANOS_PER_MILLIS = 1000000L; - static final long NANOS_PER_SECOND = 1000000000L; + static final long NANOS_PER_SECOND = 1000000000L; private static final FastDateFormat ourHumanDateFormat = FastDateFormat.getDateInstance(FastDateFormat.MEDIUM); + private static final FastDateFormat ourHumanDateTimeFormat = FastDateFormat.getDateTimeInstance(FastDateFormat.MEDIUM, FastDateFormat.MEDIUM); + private static final long serialVersionUID = 1L; private String myFractionalSeconds; private TemporalPrecisionEnum myPrecision = null; @@ -39,13 +39,13 @@ public abstract class BaseDateTimeType extends PrimitiveType { /** * Constructor * - * @throws DataFormatException + * @throws IllegalArgumentException * If the specified precision is not allowed for this type */ public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision) { setValue(theDate, thePrecision); if (isPrecisionAllowed(thePrecision) == false) { - throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + thePrecision + " precision): " + theDate); + throw new IllegalArgumentException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + thePrecision + " precision): " + theDate); } } @@ -60,16 +60,76 @@ public abstract class BaseDateTimeType extends PrimitiveType { /** * Constructor * - * @throws DataFormatException + * @throws IllegalArgumentException * If the specified precision is not allowed for this type */ public BaseDateTimeType(String theString) { setValueAsString(theString); if (isPrecisionAllowed(getPrecision()) == false) { - throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + getPrecision() + " precision): " + theString); + throw new IllegalArgumentException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + getPrecision() + " precision): " + theString); } } + /** + * Adds the given amount to the field specified by theField + * + * @param theField + * The field, uses constants from {@link Calendar} such as {@link Calendar#YEAR} + * @param theValue + * The number to add (or subtract for a negative number) + */ + public void add(int theField, int theValue) { + switch (theField) { + case Calendar.YEAR: + setValue(DateUtils.addYears(getValue(), theValue), getPrecision()); + break; + case Calendar.MONTH: + setValue(DateUtils.addMonths(getValue(), theValue), getPrecision()); + break; + case Calendar.DATE: + setValue(DateUtils.addDays(getValue(), theValue), getPrecision()); + break; + case Calendar.HOUR: + setValue(DateUtils.addHours(getValue(), theValue), getPrecision()); + break; + case Calendar.MINUTE: + setValue(DateUtils.addMinutes(getValue(), theValue), getPrecision()); + break; + case Calendar.SECOND: + setValue(DateUtils.addSeconds(getValue(), theValue), getPrecision()); + break; + case Calendar.MILLISECOND: + setValue(DateUtils.addMilliseconds(getValue(), theValue), getPrecision()); + break; + default: + throw new DataFormatException("Unknown field constant: " + theField); + } + } + + /** + * Returns true if the given object represents a date/time before this object + * + * @throws NullPointerException + * If this.getValue() or theDateTimeType.getValue() + * return null + */ + public boolean after(DateTimeType theDateTimeType) { + validateBeforeOrAfter(theDateTimeType); + return getValue().after(theDateTimeType.getValue()); + } + + /** + * Returns true if the given object represents a date/time before this object + * + * @throws NullPointerException + * If this.getValue() or theDateTimeType.getValue() + * return null + */ + public boolean before(DateTimeType theDateTimeType) { + validateBeforeOrAfter(theDateTimeType); + return getValue().before(theDateTimeType.getValue()); + } + private void clearTimeZone() { myTimeZone = null; myTimeZoneZulu = false; @@ -140,11 +200,74 @@ public abstract class BaseDateTimeType extends PrimitiveType { } } + /** + * Returns the month with 1-index, e.g. 1=the first day of the month + */ + public Integer getDay() { + return getFieldValue(Calendar.DAY_OF_MONTH); + } + /** * Returns the default precision for the given datatype */ protected abstract TemporalPrecisionEnum getDefaultPrecisionForDatatype(); + private Integer getFieldValue(int theField) { + if (getValue() == null) { + return null; + } + Calendar cal = getValueAsCalendar(); + return cal.get(theField); + } + + /** + * Returns the hour of the day in a 24h clock, e.g. 13=1pm + */ + public Integer getHour() { + return getFieldValue(Calendar.HOUR_OF_DAY); + } + + /** + * Returns the milliseconds within the current second. + *

+ * Note that this method returns the + * same value as {@link #getNanos()} but with less precision. + *

+ */ + public Integer getMillis() { + return getFieldValue(Calendar.MILLISECOND); + } + + /** + * Returns the minute of the hour in the range 0-59 + */ + public Integer getMinute() { + return getFieldValue(Calendar.MINUTE); + } + + /** + * Returns the month with 0-index, e.g. 0=January + */ + public Integer getMonth() { + return getFieldValue(Calendar.MONTH); + } + + /** + * Returns the nanoseconds within the current second + *

+ * Note that this method returns the + * same value as {@link #getMillis()} but with more precision. + *

+ */ + public Long getNanos() { + if (isBlank(myFractionalSeconds)) { + return null; + } + String retVal = StringUtils.rightPad(myFractionalSeconds, 9, '0'); + retVal = retVal.substring(0, 9); + return Long.parseLong(retVal); + } + private int getOffsetIndex(String theValueString) { int plusIndex = theValueString.indexOf('+', 16); int minusIndex = theValueString.indexOf('-', 16); @@ -171,6 +294,13 @@ public abstract class BaseDateTimeType extends PrimitiveType { return myPrecision; } + /** + * Returns the second of the minute in the range 0-59 + */ + public Integer getSecond() { + return getFieldValue(Calendar.SECOND); + } + /** * Returns the TimeZone associated with this dateTime's value. May return null if no timezone was * supplied. @@ -199,6 +329,13 @@ public abstract class BaseDateTimeType extends PrimitiveType { return cal; } + /** + * Returns the year, e.g. 2015 + */ + public Integer getYear() { + return getFieldValue(Calendar.YEAR); + } + /** * To be implemented by subclasses to indicate whether the given precision is allowed by this type */ @@ -272,7 +409,7 @@ public abstract class BaseDateTimeType extends PrimitiveType { int offsetIdx = getOffsetIndex(value); String time; if (offsetIdx == -1) { - //throwBadDateFormat(theValue); + // throwBadDateFormat(theValue); // No offset - should this be an error? time = value.substring(11); } else { @@ -334,7 +471,7 @@ public abstract class BaseDateTimeType extends PrimitiveType { myFractionalSeconds = ""; } - myPrecision = precision; + myPrecision = precision; return cal.getTime(); } @@ -354,6 +491,92 @@ public abstract class BaseDateTimeType extends PrimitiveType { return retVal; } + /** + * Sets the month with 1-index, e.g. 1=the first day of the month + */ + public BaseDateTimeType setDay(int theDay) { + setFieldValue(Calendar.DAY_OF_MONTH, theDay, null, 0, 31); + return this; + } + + private void setFieldValue(int theField, int theValue, String theFractionalSeconds, int theMinimum, int theMaximum) { + validateValueInRange(theValue, theMinimum, theMaximum); + Calendar cal; + if (getValue() == null) { + cal = new GregorianCalendar(0, 0, 0); + } else { + cal = getValueAsCalendar(); + } + if (theField != -1) { + cal.set(theField, theValue); + } + if (theFractionalSeconds != null) { + myFractionalSeconds = theFractionalSeconds; + } else if (theField == Calendar.MILLISECOND) { + myFractionalSeconds = StringUtils.leftPad(Integer.toString(theValue), 3, '0'); + } + super.setValue(cal.getTime()); + } + + /** + * Sets the hour of the day in a 24h clock, e.g. 13=1pm + */ + public BaseDateTimeType setHour(int theHour) { + setFieldValue(Calendar.HOUR_OF_DAY, theHour, null, 0, 23); + return this; + } + + /** + * Sets the milliseconds within the current second. + *

+ * Note that this method sets the + * same value as {@link #setNanos(long)} but with less precision. + *

+ */ + public BaseDateTimeType setMillis(int theMillis) { + setFieldValue(Calendar.MILLISECOND, theMillis, null, 0, 999); + return this; + } + + /** + * Sets the minute of the hour in the range 0-59 + */ + public BaseDateTimeType setMinute(int theMinute) { + setFieldValue(Calendar.MINUTE, theMinute, null, 0, 59); + return this; + } + + /** + * Sets the month with 0-index, e.g. 0=January + */ + public BaseDateTimeType setMonth(int theMonth) { + setFieldValue(Calendar.MONTH, theMonth, null, 0, 11); + return this; + } + + /** + * Sets the nanoseconds within the current second + *

+ * Note that this method sets the + * same value as {@link #setMillis(int)} but with more precision. + *

+ */ + public BaseDateTimeType setNanos(long theNanos) { + validateValueInRange(theNanos, 0, NANOS_PER_SECOND - 1); + String fractionalSeconds = StringUtils.leftPad(Long.toString(theNanos), 9, '0'); + + // Strip trailing 0s + for (int i = fractionalSeconds.length(); i > 0; i--) { + if (fractionalSeconds.charAt(i - 1) != '0') { + fractionalSeconds = fractionalSeconds.substring(0, i); + break; + } + } + int millis = (int) (theNanos / NANOS_PER_MILLIS); + setFieldValue(Calendar.MILLISECOND, millis, fractionalSeconds, 0, 999); + return this; + } + /** * Sets the precision for this datatype * @@ -367,6 +590,14 @@ public abstract class BaseDateTimeType extends PrimitiveType { updateStringValue(); } + /** + * Sets the second of the minute in the range 0-59 + */ + public BaseDateTimeType setSecond(int theSecond) { + setFieldValue(Calendar.SECOND, theSecond, null, 0, 59); + return this; + } + private BaseDateTimeType setTimeZone(String theWholeValue, String theValue) { if (isBlank(theValue)) { @@ -448,6 +679,55 @@ public abstract class BaseDateTimeType extends PrimitiveType { super.setValueAsString(theValue); } + protected void setValueAsV3String(String theV3String) { + if (StringUtils.isBlank(theV3String)) { + setValue(null); + } else { + StringBuilder b = new StringBuilder(); + String timeZone = null; + for (int i = 0; i < theV3String.length(); i++) { + char nextChar = theV3String.charAt(i); + if (nextChar == '+' || nextChar == '-' || nextChar == 'Z') { + timeZone = (theV3String.substring(i)); + break; + } + + // assertEquals("2013-02-02T20:13:03-05:00", DateAndTime.parseV3("20130202201303-0500").toString()); + if (i == 4 || i == 6) { + b.append('-'); + } else if (i == 8) { + b.append('T'); + } else if (i == 10 || i == 12) { + b.append(':'); + } + + b.append(nextChar); + } + + if (b.length() == 16) + b.append(":00"); // schema rule, must have seconds + if (timeZone != null && b.length() > 10) { + if (timeZone.length() == 5) { + b.append(timeZone.substring(0, 3)); + b.append(':'); + b.append(timeZone.substring(3)); + } else { + b.append(timeZone); + } + } + + setValueAsString(b.toString()); + } + } + + /** + * Sets the year, e.g. 2015 + */ + public BaseDateTimeType setYear(int theYear) { + setFieldValue(Calendar.YEAR, theYear, null, 0, 9999); + return this; + } + private void throwBadDateFormat(String theValue) { throw new DataFormatException("Invalid date/time format: \"" + theValue + "\""); } @@ -456,6 +736,18 @@ public abstract class BaseDateTimeType extends PrimitiveType { throw new DataFormatException("Invalid date/time format: \"" + theValue + "\": " + theMesssage); } + /** + * Returns a view of this date/time as a Calendar object. Note that the returned + * Calendar object is entirely independent from this object. Changes to the + * calendar will not affect this. + */ + public Calendar toCalendar() { + Calendar retVal = Calendar.getInstance(); + retVal.setTime(getValue()); + retVal.setTimeZone(getTimeZone()); + return retVal; + } + /** * Returns a human readable version of this date/time using the system local format. *

@@ -502,6 +794,18 @@ public abstract class BaseDateTimeType extends PrimitiveType { } } + private void validateBeforeOrAfter(DateTimeType theDateTimeType) { + if (getValue() == null) { + throw new NullPointerException("This BaseDateTimeType does not contain a value (getValue() returns null)"); + } + if (theDateTimeType == null) { + throw new NullPointerException("theDateTimeType must not be null"); + } + if (theDateTimeType.getValue() == null) { + throw new NullPointerException("The given BaseDateTimeType does not contain a value (theDateTimeType.getValue() returns null)"); + } + } + private void validateCharAtIndexIs(String theValue, int theIndex, char theChar) { if (theValue.charAt(theIndex) != theChar) { throwBadDateFormat(theValue, "Expected character '" + theChar + "' at index " + theIndex + " but found " + theValue.charAt(theIndex)); @@ -514,230 +818,10 @@ public abstract class BaseDateTimeType extends PrimitiveType { } } - /** - * Returns the year, e.g. 2015 - */ - public Integer getYear() { - return getFieldValue(Calendar.YEAR); - } - - /** - * Returns the month with 0-index, e.g. 0=January - */ - public Integer getMonth() { - return getFieldValue(Calendar.MONTH); - } - - /** - * Returns the month with 1-index, e.g. 1=the first day of the month - */ - public Integer getDay() { - return getFieldValue(Calendar.DAY_OF_MONTH); - } - - /** - * Returns the hour of the day in a 24h clock, e.g. 13=1pm - */ - public Integer getHour() { - return getFieldValue(Calendar.HOUR_OF_DAY); - } - - /** - * Returns the minute of the hour in the range 0-59 - */ - public Integer getMinute() { - return getFieldValue(Calendar.MINUTE); - } - - /** - * Returns the second of the minute in the range 0-59 - */ - public Integer getSecond() { - return getFieldValue(Calendar.SECOND); - } - - /** - * Returns the milliseconds within the current second. - *

- * Note that this method returns the - * same value as {@link #getNanos()} but with less precision. - *

- */ - public Integer getMillis() { - return getFieldValue(Calendar.MILLISECOND); - } - - /** - * Returns the nanoseconds within the current second - *

- * Note that this method returns the - * same value as {@link #getMillis()} but with more precision. - *

- */ - public Long getNanos() { - if (isBlank(myFractionalSeconds)) { - return null; - } - String retVal = StringUtils.rightPad(myFractionalSeconds, 9, '0'); - retVal = retVal.substring(0, 9); - return Long.parseLong(retVal); - } - - /** - * Sets the year, e.g. 2015 - */ - public BaseDateTimeType setYear(int theYear) { - setFieldValue(Calendar.YEAR, theYear, null, 0, 9999); - return this; - } - - /** - * Sets the month with 0-index, e.g. 0=January - */ - public BaseDateTimeType setMonth(int theMonth) { - setFieldValue(Calendar.MONTH, theMonth, null, 0, 11); - return this; - } - - /** - * Sets the month with 1-index, e.g. 1=the first day of the month - */ - public BaseDateTimeType setDay(int theDay) { - setFieldValue(Calendar.DAY_OF_MONTH, theDay, null, 0, 31); - return this; - } - - /** - * Sets the hour of the day in a 24h clock, e.g. 13=1pm - */ - public BaseDateTimeType setHour(int theHour) { - setFieldValue(Calendar.HOUR_OF_DAY, theHour, null, 0, 23); - return this; - } - - /** - * Sets the minute of the hour in the range 0-59 - */ - public BaseDateTimeType setMinute(int theMinute) { - setFieldValue(Calendar.MINUTE, theMinute, null, 0, 59); - return this; - } - - /** - * Sets the second of the minute in the range 0-59 - */ - public BaseDateTimeType setSecond(int theSecond) { - setFieldValue(Calendar.SECOND, theSecond, null, 0, 59); - return this; - } - - /** - * Sets the milliseconds within the current second. - *

- * Note that this method sets the - * same value as {@link #setNanos(long)} but with less precision. - *

- */ - public BaseDateTimeType setMillis(int theMillis) { - setFieldValue(Calendar.MILLISECOND, theMillis, null, 0, 999); - return this; - } - - /** - * Sets the nanoseconds within the current second - *

- * Note that this method sets the - * same value as {@link #setMillis(int)} but with more precision. - *

- */ - public BaseDateTimeType setNanos(long theNanos) { - validateValueInRange(theNanos, 0, NANOS_PER_SECOND-1); - String fractionalSeconds = StringUtils.leftPad(Long.toString(theNanos), 9, '0'); - - // Strip trailing 0s - for (int i = fractionalSeconds.length(); i > 0; i--) { - if (fractionalSeconds.charAt(i-1) != '0') { - fractionalSeconds = fractionalSeconds.substring(0, i); - break; - } - } - int millis = (int)(theNanos / NANOS_PER_MILLIS); - setFieldValue(Calendar.MILLISECOND, millis, fractionalSeconds, 0, 999); - return this; - } - - private void setFieldValue(int theField, int theValue, String theFractionalSeconds, int theMinimum, int theMaximum) { - validateValueInRange(theValue, theMinimum, theMaximum); - Calendar cal; - if (getValue() == null) { - cal = new GregorianCalendar(0, 0, 0); - } else { - cal = getValueAsCalendar(); - } - if (theField != -1) { - cal.set(theField, theValue); - } - if (theFractionalSeconds != null) { - myFractionalSeconds = theFractionalSeconds; - } else if (theField == Calendar.MILLISECOND) { - myFractionalSeconds = StringUtils.leftPad(Integer.toString(theValue), 3, '0'); - } - super.setValue(cal.getTime()); - } - private void validateValueInRange(long theValue, long theMinimum, long theMaximum) { if (theValue < theMinimum || theValue > theMaximum) { throw new IllegalArgumentException("Value " + theValue + " is not between allowable range: " + theMinimum + " - " + theMaximum); } } - private Integer getFieldValue(int theField) { - if (getValue() == null) { - return null; - } - Calendar cal = getValueAsCalendar(); - return cal.get(theField); - } - - protected void setValueAsV3String(String theV3String) { - if (StringUtils.isBlank(theV3String)) { - setValue(null); - } else { - StringBuilder b = new StringBuilder(); - String timeZone = null; - for (int i = 0; i < theV3String.length(); i++) { - char nextChar = theV3String.charAt(i); - if (nextChar == '+' || nextChar == '-' || nextChar == 'Z') { - timeZone = (theV3String.substring(i)); - break; - } - - // assertEquals("2013-02-02T20:13:03-05:00", DateAndTime.parseV3("20130202201303-0500").toString()); - if (i == 4 || i == 6) { - b.append('-'); - } else if (i == 8) { - b.append('T'); - } else if (i == 10 || i == 12) { - b.append(':'); - } - - b.append(nextChar); - } - - if (b.length() == 16) - b.append(":00"); // schema rule, must have seconds - if (timeZone != null && b.length() > 10) { - if (timeZone.length() ==5) { - b.append(timeZone.substring(0, 3)); - b.append(':'); - b.append(timeZone.substring(3)); - }else { - b.append(timeZone); - } - } - - setValueAsString(b.toString()); - } - } - } diff --git a/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/model/BaseDateTimeTypeDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/model/BaseDateTimeTypeDstu3Test.java index ce4433bdaff..b33bafbe6fd 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/model/BaseDateTimeTypeDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/model/BaseDateTimeTypeDstu3Test.java @@ -3,12 +3,7 @@ package org.hl7.fhir.dstu3.model; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.either; import static org.hamcrest.Matchers.endsWith; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import java.text.SimpleDateFormat; import java.time.LocalDateTime; @@ -55,6 +50,65 @@ public class BaseDateTimeTypeDstu3Test { assertEquals("1995-11-15T04:58:08Z", dt.getValueAsString()); } + + @Test + public void testAfter() { + assertTrue(new DateTimeType("2011-01-01T12:12:12Z").after(new DateTimeType("2011-01-01T12:12:11Z"))); + assertFalse(new DateTimeType("2011-01-01T12:12:11Z").after(new DateTimeType("2011-01-01T12:12:12Z"))); + assertFalse(new DateTimeType("2011-01-01T12:12:12Z").after(new DateTimeType("2011-01-01T12:12:12Z"))); + } + + @Test + public void testBefore() { + assertFalse(new DateTimeType("2011-01-01T12:12:12Z").before(new DateTimeType("2011-01-01T12:12:11Z"))); + assertTrue(new DateTimeType("2011-01-01T12:12:11Z").before(new DateTimeType("2011-01-01T12:12:12Z"))); + assertFalse(new DateTimeType("2011-01-01T12:12:12Z").before(new DateTimeType("2011-01-01T12:12:12Z"))); + } + + @Test() + public void testAfterNull() { + try { + assertTrue(new DateTimeType().after(new DateTimeType("2011-01-01T12:12:11Z"))); + fail(); + } catch (NullPointerException e) { + assertEquals("This BaseDateTimeType does not contain a value (getValue() returns null)", e.getMessage()); + } + try { + assertTrue(new DateTimeType("2011-01-01T12:12:11Z").after(new DateTimeType())); + fail(); + } catch (NullPointerException e) { + assertEquals("The given BaseDateTimeType does not contain a value (theDateTimeType.getValue() returns null)", e.getMessage()); + } + try { + assertTrue(new DateTimeType("2011-01-01T12:12:11Z").after(null)); + fail(); + } catch (NullPointerException e) { + assertEquals("theDateTimeType must not be null", e.getMessage()); + } + } + + @Test() + public void testBeforeNull1() { + try { + assertTrue(new DateTimeType().before(new DateTimeType("2011-01-01T12:12:11Z"))); + fail(); + } catch (NullPointerException e) { + assertEquals("This BaseDateTimeType does not contain a value (getValue() returns null)", e.getMessage()); + } + try { + assertTrue(new DateTimeType("2011-01-01T12:12:11Z").before(new DateTimeType())); + fail(); + } catch (NullPointerException e) { + assertEquals("The given BaseDateTimeType does not contain a value (theDateTimeType.getValue() returns null)", e.getMessage()); + } + try { + assertTrue(new DateTimeType("2011-01-01T12:12:11Z").before(null)); + fail(); + } catch (NullPointerException e) { + assertEquals("theDateTimeType must not be null", e.getMessage()); + } + } + /** * Test for #57 */ @@ -64,13 +118,13 @@ public class BaseDateTimeTypeDstu3Test { try { new DateType("2001-01-02T11:13:33"); fail(); - } catch (DataFormatException e) { + } catch (IllegalArgumentException e) { assertThat(e.getMessage(), containsString("precision")); } try { new InstantType("2001-01-02"); fail(); - } catch (DataFormatException e) { + } catch (IllegalArgumentException e) { assertThat(e.getMessage(), containsString("precision")); } }