jorge-aranda
3/10/2018 - 3:40 PM

Current LocalTime is inside of a TimeRange defined from a properties

Current LocalTime is inside of a TimeRange defined from a properties


package es.jaranda.commons.timeutils.application.rule;

import java.time.Instant;
import java.util.function.Predicate;

public interface CurrentLocalTimeIsInsideTimeRangeRule
       extends Predicate<Instant> { }

package es.jaranda.commons.timeutils.application.rule.impl;

import es.jaranda.commons.timeutils.application.properties.TimeRangeContract;
import es.jaranda.commons.timeutils.application.rule.CurrentLocalTimeIsInsideTimeRangeRule;

import java.time.Instant;
import java.time.LocalTime;
import java.time.ZoneId;

public class CurrentLocalTimeIsInsideTimeRangeRuleImpl
       implements CurrentLocalTimeIsInsideTimeRangeRule {

    private final TimeRangeContract timeRangeContract;

    public CurrentLocalTimeIsInsideTimeRangeRuleImpl(
            final TimeRangeContract timeRangeContract) {
        this.timeRangeContract = timeRangeContract;
    }

    @Override
    public boolean test(final Instant currentInstant) {
        final ZoneId zoneId = ZoneId.of(
                timeRangeContract.getAppliedTimeZoneForTimeRange()
        );
        final Integer minHour = timeRangeContract.getStartHourOfTimeRange();
        final Integer maxHour = timeRangeContract.getEndHourOfTimeRange();

        final LocalTime minLocalTime = LocalTime.of(minHour, 0);
        final LocalTime maxLocalTime = LocalTime.of(
                maxHour, 59, 59, 999_999_999
        );
        final LocalTime currentLocalTime =
                currentInstant.atZone(zoneId).toLocalTime();

        final boolean sameDayInterval =
                isSameDayInterval(minLocalTime, maxLocalTime, currentLocalTime);
        final boolean betweenDaysInterval = isBetweenDaysInterval(
                minLocalTime, maxLocalTime, currentLocalTime
        );

        return sameDayInterval || betweenDaysInterval;
    }

    private boolean isBetweenDaysInterval(final LocalTime minLocalTime,
                                          final LocalTime maxLocalTime,
                                          final LocalTime currentLocalTime) {
        return minLocalTime.isAfter(maxLocalTime) &&
                (!currentLocalTime.isAfter(minLocalTime) ||
                 !currentLocalTime.isBefore(maxLocalTime));
    }

    private boolean isSameDayInterval(final LocalTime minLocalTime,
                                      final LocalTime maxLocalTime,
                                      final LocalTime currentLocalTime) {
        return !minLocalTime.isAfter(maxLocalTime) &&
                !currentLocalTime.isAfter(maxLocalTime) &&
                !currentLocalTime.isBefore(minLocalTime);
    }
}

package es.jaranda.commons.timeutils.application.rule

import es.jaranda.commons.timeutils.application.properties.TimeRangeContract
import es.jaranda.commons.timeutils.application.rule.impl.CurrentLocalTimeIsInsideTimeRangeRuleImpl
import spock.lang.Specification

import java.time.Instant
import java.time.OffsetDateTime
import java.time.zone.ZoneRulesException

class CurrentLocalTimeIsInsideTimeRangeRuleSpec extends Specification {

    private static final String UTC_TIME_ZONE_CODE = "UTC"
    private static final String BAD_TIME_ZONE_CODE = "badTimeZone"
    private static final String VALID_ISO_8601_IN_UTC_ZONE =
            "2016-04-15T10:00:00Z"
    private static final String VALID_ISO_8601_IN_UTC_ZONE_MAX_LIMIT =
            "2016-04-15T10:59:59.999Z"

    private static final String VALID_ISO_8601_DAYLIGHT_SAVING_TIME_DAY =
            "2018-03-25T02:30:00+01:00"
    private static final String MADRID_TIME_ZONE_CODE = "Europe/Madrid"

    private final def timeRangeContractMock = Mock(TimeRangeContract)

    private final def currentLocalTimeIsInsideTimeRangeRule =
            new CurrentLocalTimeIsInsideTimeRangeRuleImpl(timeRangeContractMock)



    def "Should be is inside of range when is same day and is between"() {
        given:
            final def currentInstant = Instant.parse(VALID_ISO_8601_IN_UTC_ZONE)
            1 * timeRangeContractMock.startHourOfTimeRange >> 8
            1 * timeRangeContractMock.endHourOfTimeRange >> 11
            1 * timeRangeContractMock.appliedTimeZoneForTimeRange >>
                    UTC_TIME_ZONE_CODE
        when:
            def isInsideRange =
                currentLocalTimeIsInsideTimeRangeRule.test(currentInstant)
        then:
            isInsideRange
    }

    def "Should be is inside of range when is same day and is not between"() {
        given:
            final def currentInstant = Instant.parse(VALID_ISO_8601_IN_UTC_ZONE)
            1 * timeRangeContractMock.startHourOfTimeRange >> 11
            1 * timeRangeContractMock.endHourOfTimeRange >> 13
            1 * timeRangeContractMock.appliedTimeZoneForTimeRange >>
                    UTC_TIME_ZONE_CODE
        when:
            def isInsideRange =
                    currentLocalTimeIsInsideTimeRangeRule.test(currentInstant)
        then:
            !isInsideRange
    }

    def "Should be is inside of range when is same day and is min-limit"() {
        given:
            final def currentInstant = Instant.parse(VALID_ISO_8601_IN_UTC_ZONE)
            1 * timeRangeContractMock.startHourOfTimeRange >> 10
            1 * timeRangeContractMock.endHourOfTimeRange >> 15
            1 * timeRangeContractMock.appliedTimeZoneForTimeRange >>
                UTC_TIME_ZONE_CODE
        when:
            def isInsideRange =
                currentLocalTimeIsInsideTimeRangeRule.test(currentInstant)
        then:
            isInsideRange
    }

    def """Should be is inside of range when is same day and is max hour limit
        """() {
        given:
            final def currentInstant = Instant.parse(VALID_ISO_8601_IN_UTC_ZONE)
            1 * timeRangeContractMock.startHourOfTimeRange >> 8
            1 * timeRangeContractMock.endHourOfTimeRange >> 10
            1 * timeRangeContractMock.appliedTimeZoneForTimeRange >>
                    UTC_TIME_ZONE_CODE
        when:
            def isInsideRange =
                    currentLocalTimeIsInsideTimeRangeRule.test(currentInstant)
        then:
            isInsideRange
    }

    def "Should be is inside of range when is same day and is max-limit"() {
        given:
            final def currentInstant = Instant.parse(
                    VALID_ISO_8601_IN_UTC_ZONE_MAX_LIMIT
            )
            1 * timeRangeContractMock.startHourOfTimeRange >> 8
            1 * timeRangeContractMock.endHourOfTimeRange >> 10
            1 * timeRangeContractMock.appliedTimeZoneForTimeRange >>
                    UTC_TIME_ZONE_CODE
        when:
            def isInsideRange =
                    currentLocalTimeIsInsideTimeRangeRule.test(currentInstant)
        then:
            isInsideRange
    }

    def """Should be is inside of range when is same day and is
           min and max hour limit"""() {
        given:
        final def currentInstant = Instant.parse(VALID_ISO_8601_IN_UTC_ZONE)
        1 * timeRangeContractMock.startHourOfTimeRange >> 10
        1 * timeRangeContractMock.endHourOfTimeRange >> 10
        1 * timeRangeContractMock.appliedTimeZoneForTimeRange >>
                UTC_TIME_ZONE_CODE
        when:
        def isInsideRange =
                currentLocalTimeIsInsideTimeRangeRule.test(currentInstant)
        then:
        isInsideRange
    }

    def """Should be is inside of range when is same day and is
           min hour and max limit"""() {
        given:
        final def currentInstant = Instant.parse(
                VALID_ISO_8601_IN_UTC_ZONE_MAX_LIMIT
        )
        1 * timeRangeContractMock.startHourOfTimeRange >> 10
        1 * timeRangeContractMock.endHourOfTimeRange >> 10
        1 * timeRangeContractMock.appliedTimeZoneForTimeRange >>
                UTC_TIME_ZONE_CODE
        when:
            def isInsideRange =
                    currentLocalTimeIsInsideTimeRangeRule.test(currentInstant)
        then:
            isInsideRange
    }

    def """Should be is inside of range when is between two days and is
           between"""() {
        given:
            final def currentInstant = Instant.parse(VALID_ISO_8601_IN_UTC_ZONE)
            1 * timeRangeContractMock.startHourOfTimeRange >> 22
            1 * timeRangeContractMock.endHourOfTimeRange >> 12
            1 * timeRangeContractMock.appliedTimeZoneForTimeRange >>
                    UTC_TIME_ZONE_CODE
        when:
            def isInsideRange =
                    currentLocalTimeIsInsideTimeRangeRule.test(currentInstant)
        then:
            isInsideRange
    }

    def "Should be outside when is between two days and is not between"() {
        given:
            final def currentInstant = Instant.parse(VALID_ISO_8601_IN_UTC_ZONE)
            1 * timeRangeContractMock.startHourOfTimeRange >> 11
            1 * timeRangeContractMock.endHourOfTimeRange >> 15
            1 * timeRangeContractMock.appliedTimeZoneForTimeRange >>
                    UTC_TIME_ZONE_CODE
        when:
            def isInsideRange =
                    currentLocalTimeIsInsideTimeRangeRule.test(currentInstant)
        then:
            !isInsideRange
    }

    def "Should be outside when is daylight saving and is missing hour of day"() {
        given:
            final def currentInstant = OffsetDateTime.
                    parse(VALID_ISO_8601_DAYLIGHT_SAVING_TIME_DAY).toInstant()
            1 * timeRangeContractMock.startHourOfTimeRange >> 2
            1 * timeRangeContractMock.endHourOfTimeRange >> 2
            1 * timeRangeContractMock.appliedTimeZoneForTimeRange >>
                    MADRID_TIME_ZONE_CODE
        when:
            def isInsideRange =
                currentLocalTimeIsInsideTimeRangeRule.test(currentInstant)
        then:
            !isInsideRange
    }

    def "Should fail when bad time zone is specified"() {
        given:
        final def currentInstant = Instant.parse(VALID_ISO_8601_IN_UTC_ZONE)
        timeRangeContractMock.startHourOfTimeRange >> 8
        timeRangeContractMock.endHourOfTimeRange >> 11
        1 * timeRangeContractMock.appliedTimeZoneForTimeRange >>
                BAD_TIME_ZONE_CODE
        when:
            currentLocalTimeIsInsideTimeRangeRule.test(currentInstant)
        then:
            final ZoneRulesException ex = thrown()
            ex
    }

}


@file:JvmName("LocalTimeRangeUtils")

package es.jaranda.commons.timeutils.utils

import es.jaranda.commons.timeutils.application.properties.TimeRangeContract
import es.jaranda.commons.timeutils.application.rule.impl.CurrentLocalTimeIsInsideTimeRangeRuleImpl
import java.time.*

fun currentInstantIsInsideTimeRange(zoneId : ZoneId,
                                    startRangeHour : Int,
                                    endRangeHour: Int) =
        instantIsInsideHourTimeRange(
                Instant.now(), zoneId, startRangeHour, endRangeHour
        )

fun instantIsInsideHourTimeRange(instant : Instant,
                                 zoneId: ZoneId,
                                 startRangeHour : Int,
                                 endRangeHour: Int) =
        CurrentLocalTimeIsInsideTimeRangeRuleImpl(
                object : TimeRangeContract {
                    override fun getStartHourOfTimeRange(): Int {
                        return startRangeHour
                    }

                    override fun getEndHourOfTimeRange(): Int {
                        return endRangeHour
                    }

                    override fun getAppliedTimeZoneForTimeRange(): String {
                        return zoneId.id
                    }
                }
        ).test(instant)


fun Instant.isInsideHourTimeRange(zoneId : ZoneId,
                                  startRangeHour: Int,
                                  endRangeHour: Int) =
        instantIsInsideHourTimeRange(this, zoneId, startRangeHour, endRangeHour)

fun OffsetDateTime.isInsideHourTimeRange(startRangeHour: Int,
                                         endRangeHour: Int) =
        instantIsInsideHourTimeRange(this.toInstant(), this.offset,
                                     startRangeHour, endRangeHour)

fun ZonedDateTime.isInsideHourTimeRange(startRangeHour: Int,
                                         endRangeHour: Int) =
        instantIsInsideHourTimeRange(this.toInstant(), this.offset,
                startRangeHour, endRangeHour)

fun LocalDateTime.isInsideHourTimeRange(startRangeHour: Int,
                                        endRangeHour: Int) =
        instantIsInsideHourTimeRange(
                this.toInstant(ZoneOffset.UTC),
                ZoneOffset.UTC, startRangeHour, endRangeHour
        )

fun LocalTime.isInsideHourTimeRange(startRangeHour: Int,
                                    endRangeHour: Int) =
        instantIsInsideHourTimeRange(
                this.atDate(
                        LocalDate.of(2018,1,1)
                ).toInstant(ZoneOffset.UTC),
                ZoneOffset.UTC, startRangeHour, endRangeHour
        )

package es.jaranda.commons.timeutils.infrastructure.properties;

import es.jaranda.commons.timeutils.application.properties.TimeRangeContract;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.annotation.Validated;

@Validated
@Configuration
@ConfigurationProperties(
        prefix = "es.jaranda.commons.timeutils.examples.timerangeexample")
public class TimeRangeConfigurationProperties implements TimeRangeContract {

    private Integer startHourOfTimeRange;
    private Integer endHourOfTimeRange;
    private String appliedTimeZoneOfTimeRange;

    @Override
    public Integer getStartHourOfTimeRange() {
        return startHourOfTimeRange;
    }

    @Override
    public Integer getEndHourOfTimeRange() {
        return endHourOfTimeRange;
    }

    @Override
    public String getAppliedTimeZoneForTimeRange() {
        return appliedTimeZoneOfTimeRange;
    }

    public void setStartHourOfTimeRange(final Integer startHourOfTimeRange) {
        this.startHourOfTimeRange = startHourOfTimeRange;
    }

    public void setEndHourOfTimeRange(final Integer endHourOfTimeRange) {
        this.endHourOfTimeRange = endHourOfTimeRange;
    }

    public void setAppliedTimeZoneOfTimeRange(
            final String appliedTimeZoneOfTimeRange) {
        this.appliedTimeZoneOfTimeRange = appliedTimeZoneOfTimeRange;
    }
}

package es.jaranda.commons.timeutils.application.properties;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

public interface TimeRangeContract {
    @NotNull
    @Min(0)
    @Max(23)
    Integer getStartHourOfTimeRange();

    @NotNull
    @Min(0)
    @Max(23)
    Integer getEndHourOfTimeRange();

    @NotNull
    String getAppliedTimeZoneForTimeRange();
}