I want to implement this code to check if a user is subscribed to a service:
public String calculateSubscription(String email) {
Optional<Subscription> subscriptionsByUserEmail = subscriptionService.findSubscriptionsByUserEmail(email);
if (subscriptionsByUserEmail.isPresent()) {
Subscription subscription = subscriptionsByUserEmail.get();
LocalDateTime startAt = subscription.getStartAt();
LocalDateTime endAt = subscription.getEndAt();
LocalDateTime now = LocalDateTime.now();
if(/* do here comparison */){
return "subscribed";
}
}
return "unsubscribed";
}
Into database I store 2 fields:
@Column(name = "start_at")
@Convert(converter = LocalDateTimeConverter.class)
private LocalDateTime startAt;
@Column(name = "end_at")
@Convert(converter = LocalDateTimeConverter.class)
private LocalDateTime endAt;
I need to check if now
variable is matched between the time interval startAt
and endAt
.
How could this check be implemented into the if statement?
EDIT:
@Converter(autoApply = true)
public class LocalDateTimeConverter implements AttributeConverter<LocalDateTime, Timestamp> {
@Override
public Timestamp convertToDatabaseColumn(LocalDateTime localDateTime) {
return Optional.ofNullable(localDateTime)
.map(Timestamp::valueOf)
.orElse(null);
}
@Override
public LocalDateTime convertToEntityAttribute(Timestamp timestamp) {
return Optional.ofNullable(timestamp)
.map(Timestamp::toLocalDateTime)
.orElse(null);
}
}
Two points:
LocalDateTime
for a point in time. Anyone reading it will be free to interpret it in any time zone they can think of, for a variation of interpretations of 24 to 26 hours. Instant
is the class to use for a point in time. It has isBefore
and isAfter
methods just like LocalDateTime
, so the code will not be different. Alternatives that also do define a point in time include ZonedDateTime
and OffsetDateTime
.Optional
don’t use its isPresent
and get
methods (except in rare cases). They are low-level, and by far the most often we can obtain more elegant code with higher-level methods.I am rather into method chaining with Optional
, so my code would look like:
public String calculateSubscription(String email) {
Instant now = Instant.now();
return subscriptionService.findSubscriptionsByUserEmail(email)
.filter(sub -> ! sub.getStartAt().isAfter(now) && sub.getEndAt().isAfter(now))
.map(sub -> "subscribed")
.orElse("unsubscribed");
}
This works if getStartAt()
and getEndAt()
return Instant
. For consistent results in corner cases I read the clock only once (not twice within the filter condition). Standard interpretation of a time interval is half-open, from start inclusive to end exclusive, so my code uses this. I am using “not after” to mean “on or before”. You may not need this precision in your case.
For storing a point in time into your database, with most database engines you need a timestamp with time zone
.
Edit: To elaborate a bit, a date and time of day, which are what a LocalDateTime
gives us, do not establish a point in time. June 30 11:45 in Tokyo is 16 hours before June 30 11:45 in Los Angeles, for example. So for a point in time we additionally need either a time zone or a UTC offset.
If your database can be made to provide a point in time, typically through a timestamp with time zone
as mentioned, we’re happy. Exactly how to retrieve it into Java depends on your JPA version and implementation, so I’d rather not try to be specific on that point. You may or may not need to use OffsetDateTime
(or even ZonedDateTime
) rather than Instant
in Java or to provide a different custom converter from the one you are using now. The latter may be as easy using Timestamp::toInstant
instead of Timestamp::toLocalDateTime
.
If your database only gives you date and time of day, for example if it uses its datetime
data type and this cannot be changed, don’t try to pretend otherwise. In this case insisting on converting to Instant
may do more harm than good. No chain is stronger than its weakest link anyway. So in this case stick to LocalDateTime
throughout in Java. Then read the clock in this fashion:
ZoneId timeZoneThatTheDatabaseAssumes = ZoneId.of("Europe/Gibraltar");
LocalDateTime now = LocalDateTime.now(timeZoneThatTheDatabaseAssumes);
Of course specify the time zone ID for the time zone that your database uses.