Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions src/main/java/org/javarosa/core/model/utils/DateUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@

package org.javarosa.core.model.utils;

import org.javarosa.core.services.locale.Localization;
import org.javarosa.core.util.MathUtils;
import org.joda.time.LocalDateTime;
import org.joda.time.format.DateTimeFormat;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import org.javarosa.core.services.locale.Localization;
import org.javarosa.core.util.MathUtils;
import org.joda.time.LocalDateTime;
import org.joda.time.format.DateTimeFormat;

/**
* Static utility methods for Dates in j2me
Expand Down Expand Up @@ -556,6 +557,16 @@ public static Date roundDate (Date d) {
return getDate(f.year, f.month, f.day);
}

public static boolean isMidnight(Date d) {
Calendar c = Calendar.getInstance();
c.setTime(d);

return c.get(Calendar.HOUR_OF_DAY) == 0
&& c.get(Calendar.MINUTE) == 0
&& c.get(Calendar.SECOND) == 0
&& c.get(Calendar.MILLISECOND) == 0;
}

public static Date today () {
return roundDate(new Date());
}
Expand Down
22 changes: 20 additions & 2 deletions src/main/java/org/javarosa/test/Scenario.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.javarosa.core.model.condition.EvaluationContext;
import org.javarosa.core.model.data.BooleanData;
import org.javarosa.core.model.data.DateData;
import org.javarosa.core.model.data.DateTimeData;
import org.javarosa.core.model.data.DecimalData;
import org.javarosa.core.model.data.IAnswerData;
import org.javarosa.core.model.data.IntegerData;
Expand Down Expand Up @@ -69,10 +70,11 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.sql.Date;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
Expand Down Expand Up @@ -629,12 +631,28 @@ public AnswerResult answer(char value) {
}

/**
* Answers the question at the form index
* Answers with DateData representing the LocalDate as midnight UTC on that date.
*
* Because the resulting value represents an instant in time, viewing it in a
* non-UTC time zone may yield a different local calendar date. For example,
* 2024-01-01 is stored as 2024-01-01T00:00:00Z, which is 2023-12-31 in some
* western time zones.
*/
public AnswerResult answer(LocalDate value) {
return answer(new DateData(Date.from(value.atStartOfDay(ZoneId.of("UTC")).toInstant())));
}

/**
* Answers with either DateTimeData or DateData for the specified instant.
*
* When {@code isDateTime} is false, the instant is converted to a DateData and
* normalized according to DateData's date-only semantics.
*/
public AnswerResult answer(Instant instant, boolean isDateTime) {
Date date = Date.from(instant);
return answer(isDateTime ? new DateTimeData(date) : new DateData(date));
}

/**
* Answers the question at the form index
*/
Expand Down
11 changes: 9 additions & 2 deletions src/main/java/org/javarosa/xpath/expr/XPathFuncExpr.java
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,8 @@ public Object eval(DataInstance model, EvaluationContext evalContext) {
}
} else if (name.equals("today")) {
assertArgsCount(name, args, 0);
return DateUtils.roundDate(new Date());
DateTime dt = new DateTime();
return DateUtils.roundDate(dt.toDate());
} else if (name.equals("now")) {
assertArgsCount(name, args, 0);
return new DateTime().toDate();
Expand Down Expand Up @@ -824,7 +825,13 @@ public static String toString(Object o) {
} else if (o instanceof String) {
val = (String) o;
} else if (o instanceof Date) {
val = DateUtils.formatDate((Date) o, DateUtils.FORMAT_ISO8601);
Date d = (Date) o;
// java.util.Date does not distinguish XForms date from dateTime. DateData values
// are normalized to local midnight, so treat midnight Dates as date-only values.
// This means dateTime values exactly at midnight will also be displayed as dates.
val = DateUtils.isMidnight(d) ?
DateUtils.formatDate(d, DateUtils.FORMAT_ISO8601)
: DateUtils.formatDateTime(d, DateUtils.FORMAT_ISO8601);
} else if (o instanceof IExprDataType) {
val = ((IExprDataType) o).toString();
}
Expand Down
236 changes: 236 additions & 0 deletions src/test/java/org/javarosa/xpath/expr/DateTimeTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package org.javarosa.xpath.expr;

import org.javarosa.form.api.FormEntryCaption;
import org.javarosa.test.Scenario;
import org.javarosa.xform.parse.XFormParser;
import org.joda.time.DateTimeUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.util.Date;
import java.util.TimeZone;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.javarosa.test.BindBuilderXFormsElement.bind;
import static org.javarosa.test.XFormsElement.body;
import static org.javarosa.test.XFormsElement.head;
import static org.javarosa.test.XFormsElement.html;
import static org.javarosa.test.XFormsElement.input;
import static org.javarosa.test.XFormsElement.label;
import static org.javarosa.test.XFormsElement.mainInstance;
import static org.javarosa.test.XFormsElement.model;
import static org.javarosa.test.XFormsElement.t;
import static org.javarosa.test.XFormsElement.title;

public class DateTimeTest {
private TimeZone originalTimeZone;
private static final String SIMULATED_NOW = "1998-05-23T17:49:42.123-07:00"; // 1998-05-24 in UTC
private static final Instant SIMULATED_INSTANT = OffsetDateTime.parse(SIMULATED_NOW).toInstant();

private static final TimeZone SIMULATED_TZ = TimeZone.getTimeZone("America/Los_Angeles");

@Before
public void setUp() {
DateTimeUtils.setCurrentMillisFixed(SIMULATED_INSTANT.toEpochMilli());

originalTimeZone = TimeZone.getDefault();
TimeZone.setDefault(SIMULATED_TZ);
}

@After
public void tearDown() {
TimeZone.setDefault(originalTimeZone);

DateTimeUtils.setCurrentMillisSystem();
}

@Test
public void nowLabelOutput_isIsoOffsetDateTime() throws IOException, XFormParser.ParseException {
Scenario scenario = Scenario.init(html(
head(
title("Date time"),
model(
mainInstance(t("data id=\"date-time\"",
t("now"),
t("now_note")
)),
bind("/data/now").type("string").calculate("now()")
)
),
body(
input("/data/now_note",
label("Now: <output ref=\"/data/now\"/>"))
)));

scenario.next();
assertThat(scenario.answerOf("/data/now").getValue(), is(Date.from(SIMULATED_INSTANT)));

assertThat(scenario.answerOf("/data/now").getDisplayText(), is("23/05/98 17:49"));
FormEntryCaption nowCaption = new FormEntryCaption(scenario.getFormDef(), scenario.getCurrentIndex());
assertThat(nowCaption.getQuestionText(), is("Now: 1998-05-23T17:49:42.123-07:00"));
}

@Test
public void dateTimeStringLabelOutput_isIsoOffsetDateTime() throws IOException, XFormParser.ParseException {
Scenario scenario = Scenario.init(html(
head(
title("Date time"),
model(
mainInstance(t("data id=\"date-time\"",
t("date_time", "1998-05-23T17:49:42.123-07:00"),
t("date_time_note")
)),
bind("/data/date_time").type("string")
)
),
body(
input("/data/date_time_note",
label("Date time: <output ref=\"/data/date_time\"/>"))
)));

scenario.next();
assertThat(scenario.answerOf("/data/date_time").getValue(), is("1998-05-23T17:49:42.123-07:00"));
assertThat(scenario.answerOf("/data/date_time").getDisplayText(), is("1998-05-23T17:49:42.123-07:00"));
FormEntryCaption caption = new FormEntryCaption(scenario.getFormDef(), scenario.getCurrentIndex());
assertThat(caption.getQuestionText(), is("Date time: 1998-05-23T17:49:42.123-07:00"));
}

@Test
public void dateTimeQuestionLabelOutput_isIsoOffsetDateTime() throws IOException, XFormParser.ParseException {
Scenario scenario = Scenario.init(html(
head(
title("Date time"),
model(
mainInstance(t("data id=\"date-time\"",
t("date_time"),
t("date_time_note")
)),
bind("/data/date_time").type("dateTime")
)
),
body(
input("/data/date_time",
label("Enter a date time")),
input("/data/date_time_note",
label("Date time: <output ref=\"/data/date_time\"/>"))
)));

scenario.next();
scenario.answer(SIMULATED_INSTANT, true);
assertThat(scenario.answerOf("/data/date_time").getValue(), is(Date.from(SIMULATED_INSTANT)));
assertThat(scenario.answerOf("/data/date_time").getDisplayText(), is("23/05/98 17:49"));

scenario.next();
FormEntryCaption caption = new FormEntryCaption(scenario.getFormDef(), scenario.getCurrentIndex());
assertThat(caption.getQuestionText(), is("Date time: 1998-05-23T17:49:42.123-07:00"));
}

@Test
public void todayLabelOutput_isIsoDate() throws IOException, XFormParser.ParseException {
Date expectedDay = Date.from(
SIMULATED_INSTANT.atZone(SIMULATED_TZ.toZoneId())
.toLocalDate()
.atStartOfDay(SIMULATED_TZ.toZoneId())
.toInstant()
);

Scenario scenario = Scenario.init(html(
head(
title("Date"),
model(
mainInstance(t("data id=\"date\"",
t("today"),
t("today_note")
)),
bind("/data/today").type("date").calculate("today()")
)
),
body(
input("/data/today_note",
label("Today: <output ref=\"/data/today\"/>"))
)));

scenario.next();
assertThat(scenario.answerOf("/data/today").getValue(), is(expectedDay));

assertThat(scenario.answerOf("/data/today").getDisplayText(), is("23/05/98"));
FormEntryCaption nowCaption = new FormEntryCaption(scenario.getFormDef(), scenario.getCurrentIndex());
assertThat(nowCaption.getQuestionText(), is("Today: 1998-05-23"));
}

// Date field type, now() expression
@Test
public void nowDateLabelOutput_isIsoDate() throws IOException, XFormParser.ParseException {
Date expectedDay = Date.from(
SIMULATED_INSTANT.atZone(SIMULATED_TZ.toZoneId())
.toLocalDate()
.atStartOfDay(SIMULATED_TZ.toZoneId())
.toInstant()
);

Scenario scenario = Scenario.init(html(
head(
title("Now date"),
model(
mainInstance(t("data id=\"now-date\"",
t("now"),
t("now_note")
)),
bind("/data/now").type("date").calculate("now()")
)
),
body(
input("/data/now_note",
label("Today: <output ref=\"/data/now\"/>"))
)));

scenario.next();
assertThat(scenario.answerOf("/data/now").getValue(), is(expectedDay));

assertThat(scenario.answerOf("/data/now").getDisplayText(), is("23/05/98"));
FormEntryCaption nowCaption = new FormEntryCaption(scenario.getFormDef(), scenario.getCurrentIndex());
assertThat(nowCaption.getQuestionText(), is("Today: 1998-05-23"));
}

@Test
public void dateQuestionLabelOutput_isIsoDate() throws IOException, XFormParser.ParseException {
Date expectedDay = Date.from(
SIMULATED_INSTANT.atZone(SIMULATED_TZ.toZoneId())
.toLocalDate()
.atStartOfDay(SIMULATED_TZ.toZoneId())
.toInstant()
);

Scenario scenario = Scenario.init(html(
head(
title("Date"),
model(
mainInstance(t("data id=\"date\"",
t("date"),
t("date_note")
)),
bind("/data/date").type("date")
)
),
body(
input("/data/date",
label("Enter a date")),
input("/data/date_note",
label("Date: <output ref=\"/data/date\"/>"))
)));

scenario.next();
scenario.answer(SIMULATED_INSTANT, false);
assertThat(scenario.answerOf("/data/date").getValue(), is(expectedDay));
assertThat(scenario.answerOf("/data/date").getDisplayText(), is("23/05/98"));

scenario.next();
FormEntryCaption caption = new FormEntryCaption(scenario.getFormDef(), scenario.getCurrentIndex());
assertThat(caption.getQuestionText(), is("Date: 1998-05-23"));
}
}