mirror of https://github.com/apache/lucene.git
SOLR-2772: Fixed Date parsing/formatting of years 0001-1000
git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1171691 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
72106bb37b
commit
82b72ac3fe
|
@ -345,6 +345,8 @@ Bug Fixes
|
||||||
* SOLR-2726: Fixed NullPointerException when using spellcheck.q with Suggester.
|
* SOLR-2726: Fixed NullPointerException when using spellcheck.q with Suggester.
|
||||||
(Bernd Fehling, valentin via rmuir)
|
(Bernd Fehling, valentin via rmuir)
|
||||||
|
|
||||||
|
* SOLR-2772: Fixed Date parsing/formatting of years 0001-1000 (hossman)
|
||||||
|
|
||||||
Other Changes
|
Other Changes
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ import org.apache.solr.response.transform.DocTransformer;
|
||||||
import org.apache.solr.response.transform.TransformContext;
|
import org.apache.solr.response.transform.TransformContext;
|
||||||
import org.apache.solr.schema.IndexSchema;
|
import org.apache.solr.schema.IndexSchema;
|
||||||
import org.apache.solr.schema.SchemaField;
|
import org.apache.solr.schema.SchemaField;
|
||||||
|
import org.apache.solr.schema.DateField;
|
||||||
import org.apache.solr.search.DocList;
|
import org.apache.solr.search.DocList;
|
||||||
import org.apache.solr.search.ReturnFields;
|
import org.apache.solr.search.ReturnFields;
|
||||||
|
|
||||||
|
@ -318,59 +319,7 @@ public abstract class TextResponseWriter {
|
||||||
|
|
||||||
|
|
||||||
public void writeDate(String name, Date val) throws IOException {
|
public void writeDate(String name, Date val) throws IOException {
|
||||||
// using a stringBuilder for numbers can be nice since
|
writeDate(name, DateField.formatExternal(val));
|
||||||
// a temporary string isn't used (it's added directly to the
|
|
||||||
// builder's buffer.
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
if (cal==null) cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"), Locale.US);
|
|
||||||
cal.setTime(val);
|
|
||||||
|
|
||||||
int i = cal.get(Calendar.YEAR);
|
|
||||||
sb.append(i);
|
|
||||||
sb.append('-');
|
|
||||||
i = cal.get(Calendar.MONTH) + 1; // 0 based, so add 1
|
|
||||||
if (i<10) sb.append('0');
|
|
||||||
sb.append(i);
|
|
||||||
sb.append('-');
|
|
||||||
i=cal.get(Calendar.DAY_OF_MONTH);
|
|
||||||
if (i<10) sb.append('0');
|
|
||||||
sb.append(i);
|
|
||||||
sb.append('T');
|
|
||||||
i=cal.get(Calendar.HOUR_OF_DAY); // 24 hour time format
|
|
||||||
if (i<10) sb.append('0');
|
|
||||||
sb.append(i);
|
|
||||||
sb.append(':');
|
|
||||||
i=cal.get(Calendar.MINUTE);
|
|
||||||
if (i<10) sb.append('0');
|
|
||||||
sb.append(i);
|
|
||||||
sb.append(':');
|
|
||||||
i=cal.get(Calendar.SECOND);
|
|
||||||
if (i<10) sb.append('0');
|
|
||||||
sb.append(i);
|
|
||||||
i=cal.get(Calendar.MILLISECOND);
|
|
||||||
if (i != 0) {
|
|
||||||
sb.append('.');
|
|
||||||
if (i<100) sb.append('0');
|
|
||||||
if (i<10) sb.append('0');
|
|
||||||
sb.append(i);
|
|
||||||
|
|
||||||
// handle canonical format specifying fractional
|
|
||||||
// seconds shall not end in '0'. Given the slowness of
|
|
||||||
// integer div/mod, simply checking the last character
|
|
||||||
// is probably the fastest way to check.
|
|
||||||
int lastIdx = sb.length()-1;
|
|
||||||
if (sb.charAt(lastIdx)=='0') {
|
|
||||||
lastIdx--;
|
|
||||||
if (sb.charAt(lastIdx)=='0') {
|
|
||||||
lastIdx--;
|
|
||||||
}
|
|
||||||
sb.setLength(lastIdx+1);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
sb.append('Z');
|
|
||||||
writeDate(name, sb.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -256,22 +256,29 @@ public class DateField extends FieldType {
|
||||||
* Thread safe method that can be used by subclasses to format a Date
|
* Thread safe method that can be used by subclasses to format a Date
|
||||||
* using the Internal representation.
|
* using the Internal representation.
|
||||||
*/
|
*/
|
||||||
protected String formatDate(Date d) {
|
public static String formatDate(Date d) {
|
||||||
return fmtThreadLocal.get().format(d);
|
return fmtThreadLocal.get().format(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the standard human readable form of the date
|
* Return the standard human readable form of the date
|
||||||
*/
|
*/
|
||||||
|
public static String formatExternal(Date d) {
|
||||||
|
return fmtThreadLocal.get().format(d) + 'Z';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see {#formatExternal}
|
||||||
|
*/
|
||||||
public String toExternal(Date d) {
|
public String toExternal(Date d) {
|
||||||
return fmtThreadLocal.get().format(d) + 'Z';
|
return formatExternal(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thread safe method that can be used by subclasses to parse a Date
|
* Thread safe method that can be used by subclasses to parse a Date
|
||||||
* that is already in the internal representation
|
* that is already in the internal representation
|
||||||
*/
|
*/
|
||||||
protected Date parseDate(String s) throws ParseException {
|
public static Date parseDate(String s) throws ParseException {
|
||||||
return fmtThreadLocal.get().parse(s);
|
return fmtThreadLocal.get().parse(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -381,9 +388,13 @@ public class DateField extends FieldType {
|
||||||
super.format(d, toAppendTo, pos);
|
super.format(d, toAppendTo, pos);
|
||||||
/* worry aboutthe milliseconds ourselves */
|
/* worry aboutthe milliseconds ourselves */
|
||||||
long millis = d.getTime() % 1000l;
|
long millis = d.getTime() % 1000l;
|
||||||
if (0l == millis) {
|
if (0L == millis) {
|
||||||
return toAppendTo;
|
return toAppendTo;
|
||||||
}
|
}
|
||||||
|
if (millis < 0L) {
|
||||||
|
// original date was prior to epoch
|
||||||
|
millis += 1000L;
|
||||||
|
}
|
||||||
int posBegin = toAppendTo.length();
|
int posBegin = toAppendTo.length();
|
||||||
toAppendTo.append(millisFormat.format(millis / 1000d));
|
toAppendTo.append(millisFormat.format(millis / 1000d));
|
||||||
if (DateFormat.MILLISECOND_FIELD == pos.getField()) {
|
if (DateFormat.MILLISECOND_FIELD == pos.getField()) {
|
||||||
|
|
|
@ -594,6 +594,7 @@ public class BasicFunctionalityTest extends SolrTestCaseJ4 {
|
||||||
/** @see org.apache.solr.util.DateMathParserTest */
|
/** @see org.apache.solr.util.DateMathParserTest */
|
||||||
@Test
|
@Test
|
||||||
public void testDateMath() {
|
public void testDateMath() {
|
||||||
|
clearIndex();
|
||||||
|
|
||||||
// testing everything from query level is hard because
|
// testing everything from query level is hard because
|
||||||
// time marches on ... and there is no easy way to reach into the
|
// time marches on ... and there is no easy way to reach into the
|
||||||
|
@ -640,6 +641,19 @@ public class BasicFunctionalityTest extends SolrTestCaseJ4 {
|
||||||
req("q", "bday:[NOW-1MONTH TO NOW+2HOURS]"), "*[count(//doc)=4]");
|
req("q", "bday:[NOW-1MONTH TO NOW+2HOURS]"), "*[count(//doc)=4]");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testDateRoundtrip() {
|
||||||
|
assertU(adoc("id", "99", "bday", "99-01-01T12:34:56.789Z"));
|
||||||
|
assertU(commit());
|
||||||
|
assertQ("year should be canonicallized to 4 digits",
|
||||||
|
req("q", "id:99"),
|
||||||
|
"//date[@name='bday'][.='0099-01-01T12:34:56.789Z']");
|
||||||
|
assertU(adoc("id", "99", "bday", "1999-01-01T12:34:56.900Z"));
|
||||||
|
assertU(commit());
|
||||||
|
assertQ("millis should be canonicallized to no trailing zeros",
|
||||||
|
req("q", "id:99"),
|
||||||
|
"//date[@name='bday'][.='1999-01-01T12:34:56.9Z']");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPatternReplaceFilter() {
|
public void testPatternReplaceFilter() {
|
||||||
|
|
|
@ -21,6 +21,9 @@ import org.apache.lucene.document.Field;
|
||||||
import org.apache.lucene.index.IndexableField;
|
import org.apache.lucene.index.IndexableField;
|
||||||
import org.apache.lucene.util.LuceneTestCase;
|
import org.apache.lucene.util.LuceneTestCase;
|
||||||
import org.apache.solr.util.DateMathParser;
|
import org.apache.solr.util.DateMathParser;
|
||||||
|
|
||||||
|
import org.junit.Ignore;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
@ -58,7 +61,7 @@ public class DateFieldTest extends LuceneTestCase {
|
||||||
assertToI("1995-12-31T23:59:59", "1995-12-31T23:59:59.000Z");
|
assertToI("1995-12-31T23:59:59", "1995-12-31T23:59:59.000Z");
|
||||||
assertToI("1995-12-31T23:59:59", "1995-12-31T23:59:59.00Z");
|
assertToI("1995-12-31T23:59:59", "1995-12-31T23:59:59.00Z");
|
||||||
assertToI("1995-12-31T23:59:59", "1995-12-31T23:59:59.0Z");
|
assertToI("1995-12-31T23:59:59", "1995-12-31T23:59:59.0Z");
|
||||||
|
|
||||||
// kind of kludgy, but we have other tests for the actual date math
|
// kind of kludgy, but we have other tests for the actual date math
|
||||||
assertToI(f.toInternal(p.parseMath("/DAY")), "NOW/DAY");
|
assertToI(f.toInternal(p.parseMath("/DAY")), "NOW/DAY");
|
||||||
|
|
||||||
|
@ -100,21 +103,107 @@ public class DateFieldTest extends LuceneTestCase {
|
||||||
|
|
||||||
// as of Solr1.3
|
// as of Solr1.3
|
||||||
public void testToObject() throws Exception {
|
public void testToObject() throws Exception {
|
||||||
|
|
||||||
|
// just after epoch
|
||||||
|
assertToObject( 5L, "1970-01-01T00:00:00.005Z");
|
||||||
|
assertToObject( 0L, "1970-01-01T00:00:00Z");
|
||||||
|
assertToObject(370L, "1970-01-01T00:00:00.37Z");
|
||||||
|
assertToObject(900L, "1970-01-01T00:00:00.9Z");
|
||||||
|
|
||||||
|
// well after epoch
|
||||||
assertToObject(820454399987l, "1995-12-31T23:59:59.987666Z");
|
assertToObject(820454399987l, "1995-12-31T23:59:59.987666Z");
|
||||||
assertToObject(820454399987l, "1995-12-31T23:59:59.987Z");
|
assertToObject(820454399987l, "1995-12-31T23:59:59.987Z");
|
||||||
assertToObject(820454399980l, "1995-12-31T23:59:59.98Z");
|
assertToObject(820454399980l, "1995-12-31T23:59:59.98Z");
|
||||||
assertToObject(820454399900l, "1995-12-31T23:59:59.9Z");
|
assertToObject(820454399900l, "1995-12-31T23:59:59.9Z");
|
||||||
assertToObject(820454399000l, "1995-12-31T23:59:59Z");
|
assertToObject(820454399000l, "1995-12-31T23:59:59Z");
|
||||||
|
|
||||||
|
// waaaay after epoch
|
||||||
|
assertToObject(327434918399005L, "12345-12-31T23:59:59.005Z");
|
||||||
|
assertToObject(327434918399000L, "12345-12-31T23:59:59Z");
|
||||||
|
assertToObject(327434918399370L, "12345-12-31T23:59:59.37Z");
|
||||||
|
assertToObject(327434918399900L, "12345-12-31T23:59:59.9Z");
|
||||||
|
|
||||||
|
// well before epoch
|
||||||
|
assertToObject(-52700112001000L, "0299-12-31T23:59:59Z");
|
||||||
|
assertToObject(-52700112000877L, "0299-12-31T23:59:59.123Z");
|
||||||
|
assertToObject(-52700112000910L, "0299-12-31T23:59:59.09Z");
|
||||||
|
|
||||||
|
// flexible in parsing years less then 4 digits
|
||||||
|
assertToObject(-52700112001000L, "299-12-31T23:59:59Z");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testFormatter() {
|
public void testFormatter() {
|
||||||
assertEquals("1970-01-01T00:00:00.005", f.formatDate(new Date(5)));
|
// just after epoch
|
||||||
assertEquals("1970-01-01T00:00:00", f.formatDate(new Date(0)));
|
assertFormat("1970-01-01T00:00:00.005", 5L);
|
||||||
assertEquals("1970-01-01T00:00:00.37", f.formatDate(new Date(370)));
|
assertFormat("1970-01-01T00:00:00", 0L);
|
||||||
assertEquals("1970-01-01T00:00:00.9", f.formatDate(new Date(900)));
|
assertFormat("1970-01-01T00:00:00.37", 370L);
|
||||||
|
assertFormat("1970-01-01T00:00:00.9", 900L);
|
||||||
|
|
||||||
|
// well after epoch
|
||||||
|
assertFormat("1999-12-31T23:59:59.005", 946684799005L);
|
||||||
|
assertFormat("1999-12-31T23:59:59", 946684799000L);
|
||||||
|
assertFormat("1999-12-31T23:59:59.37", 946684799370L);
|
||||||
|
assertFormat("1999-12-31T23:59:59.9", 946684799900L);
|
||||||
|
|
||||||
|
// waaaay after epoch
|
||||||
|
assertFormat("12345-12-31T23:59:59.005", 327434918399005L);
|
||||||
|
assertFormat("12345-12-31T23:59:59", 327434918399000L);
|
||||||
|
assertFormat("12345-12-31T23:59:59.37", 327434918399370L);
|
||||||
|
assertFormat("12345-12-31T23:59:59.9", 327434918399900L);
|
||||||
|
|
||||||
|
// well before epoch
|
||||||
|
assertFormat("0299-12-31T23:59:59", -52700112001000L);
|
||||||
|
assertFormat("0299-12-31T23:59:59.123", -52700112000877L);
|
||||||
|
assertFormat("0299-12-31T23:59:59.09", -52700112000910L);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Using dates in the canonical format, verify that parsing+formating
|
||||||
|
* is an identify function
|
||||||
|
*/
|
||||||
|
public void testRoundTrip() throws Exception {
|
||||||
|
|
||||||
|
// typical dates, various precision
|
||||||
|
assertRoundTrip("1995-12-31T23:59:59.987Z");
|
||||||
|
assertRoundTrip("1995-12-31T23:59:59.98Z");
|
||||||
|
assertRoundTrip("1995-12-31T23:59:59.9Z");
|
||||||
|
assertRoundTrip("1995-12-31T23:59:59Z");
|
||||||
|
assertRoundTrip("1976-03-06T03:06:00Z");
|
||||||
|
|
||||||
|
// dates with atypical years
|
||||||
|
assertRoundTrip("0001-01-01T01:01:01Z");
|
||||||
|
assertRoundTrip("12021-12-01T03:03:03Z");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("SOLR-2773: Non-Positive years don't work")
|
||||||
|
public void testRoundTripNonPositiveYear() throws Exception {
|
||||||
|
|
||||||
|
// :TODO: ambiguity about year zero
|
||||||
|
// assertRoundTrip("0000-04-04T04:04:04Z");
|
||||||
|
|
||||||
|
// dates with negative years
|
||||||
|
assertRoundTrip("-0005-05-05T05:05:05Z");
|
||||||
|
assertRoundTrip("-2021-12-01T04:04:04Z");
|
||||||
|
assertRoundTrip("-12021-12-01T02:02:02Z");
|
||||||
|
|
||||||
|
// :TODO: assertFormat and assertToObject some negative years
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void assertFormat(final String expected, final long millis) {
|
||||||
|
assertEquals(expected, f.formatDate(new Date(millis)));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void assertRoundTrip(String canonicalDate) throws Exception {
|
||||||
|
Date d = DateField.parseDate(canonicalDate);
|
||||||
|
String result = DateField.formatDate(d) + "Z";
|
||||||
|
assertEquals("d:" + d.getTime(), canonicalDate, result);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void testCreateField() {
|
public void testCreateField() {
|
||||||
int props = FieldProperties.INDEXED ^ FieldProperties.STORED;
|
int props = FieldProperties.INDEXED ^ FieldProperties.STORED;
|
||||||
SchemaField sf = new SchemaField( "test", f, props, null );
|
SchemaField sf = new SchemaField( "test", f, props, null );
|
||||||
|
|
|
@ -85,7 +85,7 @@ public class TestRangeQuery extends SolrTestCaseJ4 {
|
||||||
String[] longs = {""+(l-1), ""+(l), ""+(l+1), ""+(l-2), ""+(l+2)};
|
String[] longs = {""+(l-1), ""+(l), ""+(l+1), ""+(l-2), ""+(l+2)};
|
||||||
String[] doubles = {""+(d-1e-16), ""+(d), ""+(d+1e-16), ""+(d-2e-16), ""+(d+2e-16)};
|
String[] doubles = {""+(d-1e-16), ""+(d), ""+(d+1e-16), ""+(d-2e-16), ""+(d+2e-16)};
|
||||||
String[] strings = {"aaa","bbb","ccc", "aa","cccc" };
|
String[] strings = {"aaa","bbb","ccc", "aa","cccc" };
|
||||||
String[] dates = {"1999-12-31T23:59:59.999Z","2000-01-01T00:00:00.000Z","2000-01-01T00:00:00.001Z", "1999-12-31T23:59:59.998Z","2000-01-01T00:00:00.002Z" };
|
String[] dates = {"0299-12-31T23:59:59.999Z","2000-01-01T00:00:00.000Z","2000-01-01T00:00:00.001Z", "0299-12-31T23:59:59.998Z","2000-01-01T00:00:00.002Z" };
|
||||||
|
|
||||||
// fields that normal range queries should work on
|
// fields that normal range queries should work on
|
||||||
Map<String,String[]> norm_fields = new HashMap<String,String[]>();
|
Map<String,String[]> norm_fields = new HashMap<String,String[]>();
|
||||||
|
@ -285,4 +285,4 @@ public class TestRangeQuery extends SolrTestCaseJ4 {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue