RegularTimeSeriesIndex.java

/**
 * Copyright (c) 2017, RTE (http://www.rte-france.com)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 * SPDX-License-Identifier: MPL-2.0
 */
package com.powsybl.timeseries;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import org.threeten.extra.Interval;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.math.BigInteger;
import java.time.Duration;
import java.time.Instant;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static com.powsybl.timeseries.TimeSeries.parseNanosToInstant;
import static com.powsybl.timeseries.TimeSeries.writeInstantToNanoString;

/**
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
public class RegularTimeSeriesIndex extends AbstractTimeSeriesIndex {

    public static final String TYPE = "regularIndex";

    // Long.MAX_VALUE seconds corresponds to 292 years: Long.MAX_VALUE / (60 * 60 * 24 * 365) = 292.47
    private static final Duration MAX_DAYS = Duration.ofDays(365L * 200);

    private final Instant startInstant;
    private final Instant endInstant;
    private final Duration timeStep;

    // computed from the previous fields; startTime and endTime are inclusive,
    // with rounding to have easier interactions with calendar dates (the
    // number of milliseconds in a calendar date is not fixed, because of leap
    // seconds, daylight saving time, and leap years), so if we didn't round
    // and took the floor or the ceiling, it would give surprising results
    // between 2 calendar dates.
    private final int pointCount;

    public RegularTimeSeriesIndex(Instant startInstant, Instant endInstant, Duration timeStep) {
        if (timeStep.isNegative()) {
            throw new IllegalArgumentException("Bad timeStep value " + timeStep);
        }
        if (timeStep.compareTo(Duration.between(startInstant, endInstant)) > 0) {
            throw new IllegalArgumentException("TimeStep " + timeStep + " is longer than interval " + (Duration.between(startInstant, endInstant)));
        }
        long computedPointCount = computePointCount(startInstant, endInstant, timeStep);
        if (computedPointCount > Integer.MAX_VALUE) {
            throw new IllegalArgumentException("Point Count " + computedPointCount + " is bigger than max allowed value " + Integer.MAX_VALUE);
        }
        this.startInstant = startInstant;
        this.endInstant = endInstant;
        this.timeStep = timeStep;
        this.pointCount = (int) computedPointCount;
    }

    /**
     * @deprecated Replaced by {@link RegularTimeSeriesIndex#RegularTimeSeriesIndex(Instant, Instant, Duration)}
     */
    @Deprecated(since = "6.7.0")
    public RegularTimeSeriesIndex(long startTime, long endTime, long spacing) {
        this(Instant.ofEpochMilli(startTime),
            Instant.ofEpochMilli(endTime),
            Duration.ofMillis(spacing));
    }

    public static RegularTimeSeriesIndex create(Instant start, Instant end, Duration spacing) {
        Objects.requireNonNull(start);
        Objects.requireNonNull(end);
        Objects.requireNonNull(spacing);
        return new RegularTimeSeriesIndex(start, end, spacing);
    }

    public static RegularTimeSeriesIndex create(Interval interval, Duration spacing) {
        Objects.requireNonNull(interval);
        return create(interval.getStart(), interval.getEnd(), spacing);
    }

    public static RegularTimeSeriesIndex parseJson(JsonParser parser) {
        Objects.requireNonNull(parser);
        JsonToken token;
        try {
            Instant startInstant = null;
            Instant endInstant = null;
            Duration timeStep = null;
            while ((token = parser.nextToken()) != null) {
                switch (token) {
                    case FIELD_NAME -> {
                        String fieldName = parser.currentName();
                        switch (fieldName) {
                            // Precision in ms
                            case "startTime" -> startInstant = Instant.ofEpochMilli(parser.nextLongValue(-1));
                            case "endTime" -> endInstant = Instant.ofEpochMilli(parser.nextLongValue(-1));
                            case "spacing" -> timeStep = Duration.ofMillis(parser.nextLongValue(-1));
                            // Precision in ns
                            case "startInstant" -> startInstant = parseNanoTokenToInstant(parser);
                            case "endInstant" -> endInstant = parseNanoTokenToInstant(parser);
                            case "timeStep" -> timeStep = Duration.ofNanos(parser.nextLongValue(-1));
                            default -> throw new IllegalStateException("Unexpected field " + fieldName);
                        }
                    }
                    case END_OBJECT -> {
                        if (startInstant == null || endInstant == null || timeStep == null) {
                            throw new IllegalStateException("Incomplete regular time series index json");
                        }
                        return new RegularTimeSeriesIndex(startInstant, endInstant, timeStep);
                    }
                    default -> {
                        // Do nothing
                    }
                }
            }
            throw new IllegalStateException("Should not happen");
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    /**
     * @deprecated Replaced by {@link RegularTimeSeriesIndex#getStartInstant()}
     */
    @Deprecated(since = "6.7.0")
    public long getStartTime() {
        return startInstant.toEpochMilli();
    }

    public Instant getStartInstant() {
        return startInstant;
    }

    /**
     * @deprecated Replaced by {@link RegularTimeSeriesIndex#getEndInstant()}
     */
    @Deprecated(since = "6.7.0")
    public long getEndTime() {
        return endInstant.toEpochMilli();
    }

    public Instant getEndInstant() {
        return endInstant;
    }

    /**
     * @deprecated Replaced by {@link RegularTimeSeriesIndex#getTimeStep()}
     */
    @Deprecated(since = "6.7.0")
    public long getSpacing() {
        return timeStep.toMillis();
    }

    public Duration getTimeStep() {
        return timeStep;
    }

    private static long computePointCount(Instant startTime, Instant endTime, Duration spacing) {
        // Checks to avoid invalid duration and instants
        if (startTime == null || endTime == null || spacing == null) {
            throw new IllegalArgumentException("startTime, endTime, and spacing cannot be null.");
        }

        Duration duration = Duration.between(startTime, endTime);

        if (duration.compareTo(MAX_DAYS) > 0 || spacing.compareTo(MAX_DAYS) > 0) {
            throw new IllegalArgumentException("Time range or spacing exceeds " + MAX_DAYS.toDays() + " days.");
        }

        return Math.round(((double) (duration.toNanos())) / spacing.toNanos()) + 1;
    }

    @Override
    public int getPointCount() {
        return pointCount;
    }

    @Override
    public Instant getInstantAt(int point) {
        return startInstant.plus(timeStep.multipliedBy(point));
    }

    @Override
    public int hashCode() {
        return Objects.hash(startInstant, endInstant, timeStep);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof TimeSeriesIndex) {
            RegularTimeSeriesIndex otherIndex = (RegularTimeSeriesIndex) obj;
            return startInstant.equals(otherIndex.startInstant) &&
                endInstant.equals(otherIndex.endInstant) &&
                timeStep.equals(otherIndex.timeStep);
        }
        return false;
    }

    @Override
    public String getType() {
        return TYPE;
    }

    /**
     * <p>Writes the index in a JSON format.</p>
     * <p>If {@code timeFormat = ExportFormat.MILLISECONDS}, values are written in millisecond precision. Else, if
     * {@code timeFormat = ExportFormat.NANOSECONDS}, values are written in nanosecond precision</p>
     */
    @Override
    public void writeJson(JsonGenerator generator, ExportFormat timeFormat) {
        Objects.requireNonNull(generator);
        try {
            generator.writeStartObject();
            if (timeFormat == ExportFormat.MILLISECONDS) {
                generator.writeNumberField("startTime", startInstant.toEpochMilli());
                generator.writeNumberField("endTime", endInstant.toEpochMilli());
                generator.writeNumberField("spacing", timeStep.toMillis());
            } else {
                generator.writeNumberField("startInstant", new BigInteger(writeInstantToNanoString(startInstant)));
                generator.writeNumberField("endInstant", new BigInteger(writeInstantToNanoString(endInstant)));
                generator.writeNumberField("timeStep", timeStep.toNanos());
            }
            generator.writeEndObject();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @Override
    public Iterator<Instant> iterator() {
        return new Iterator<>() {

            Instant time = startInstant;

            @Override
            public boolean hasNext() {
                return time.compareTo(endInstant) <= 0;
            }

            @Override
            public Instant next() {
                if (!hasNext()) {
                    throw new NoSuchElementException();
                }
                Instant instant = time;
                time = time.plus(timeStep);
                return instant;
            }
        };
    }

    @Override
    public Stream<Instant> stream() {
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator(),
                                                                        Spliterator.ORDERED | Spliterator.IMMUTABLE),
                                    false);
    }

    @Override
    public String toString() {
        return "RegularTimeSeriesIndex(startInstant=" + startInstant + ", endInstant=" + endInstant + ", timeStep=" + timeStep + ")";
    }

    private static Instant parseNanoTokenToInstant(JsonParser parser) throws IOException {
        // The next token contains the value
        parser.nextToken();

        // Parse the value
        return parseNanosToInstant(parser.getValueAsString());
    }
}