diff --git a/src/main/java/ru/ulstu/TimeSeriesUtils.java b/src/main/java/ru/ulstu/TimeSeriesUtils.java new file mode 100644 index 0000000..d79e422 --- /dev/null +++ b/src/main/java/ru/ulstu/TimeSeriesUtils.java @@ -0,0 +1,35 @@ +package ru.ulstu; + +import ru.ulstu.models.TimeSeries; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +public class TimeSeriesUtils { + /** + * Вычисляет среднее значение между датами временного ряда + * + * @param timeSeries объект, содержащий временной ряд + * @return средняя разница между датами исходного временного ряда в миллисекундах + */ + public static long getTimeDifferenceInMilliseconds(TimeSeries timeSeries) { + long diffMilliseconds = 0; + for (int i = 1; i < timeSeries.getLength(); i++) { + diffMilliseconds += timeSeries.getValues().get(i - 1).getDate() + .until(timeSeries.getValues().get(i).getDate(), ChronoUnit.MILLIS); + } + return diffMilliseconds / (timeSeries.getLength() - 1); + } + + public static TimeSeries fillDates(TimeSeries timeSeries, long milliseconds) { + timeSeries.getLastValue().setDate(LocalDateTime.now()); + for (int i = timeSeries.getLength() - 2; i >= 0; i--) { + timeSeries.getValues().get(i).setDate(timeSeries.getValues().get(i + 1).getDate().minus(milliseconds, ChronoUnit.MILLIS)); + } + return timeSeries; + } + + public static TimeSeries fillDates(TimeSeries timeSeries) { + return fillDates(timeSeries, 1000 * 60 * 60 * 24); + } +} diff --git a/src/main/java/ru/ulstu/controllers/AdviceController.java b/src/main/java/ru/ulstu/controllers/AdviceController.java new file mode 100644 index 0000000..de504d1 --- /dev/null +++ b/src/main/java/ru/ulstu/controllers/AdviceController.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020. Anton Romanov + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.ulstu.controllers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.HttpServerErrorException; +import ru.ulstu.models.exceptions.ForecastValidateException; +import ru.ulstu.models.exceptions.TimeSeriesValidateException; +import ru.ulstu.models.response.ErrorConstants; +import ru.ulstu.models.response.ResponseExtended; + +@RestController +@ControllerAdvice +public class AdviceController { + private final Logger log = LoggerFactory.getLogger(AdviceController.class); + + private ResponseExtended handleException(ErrorConstants error, E errorData) { + log.warn(error.toString()); + return new ResponseExtended<>(error, errorData); + } + + @ExceptionHandler(Exception.class) + public ResponseExtended handleUnknownException(Throwable e) { + e.printStackTrace(); + return handleException(ErrorConstants.UNKNOWN, e.getMessage()); + } + + @ExceptionHandler(HttpServerErrorException.class) + public ResponseExtended handleHttpClientException(Throwable e) { + return handleException(ErrorConstants.HTTP_CLIENT_ERROR, e.getMessage()); + } + + @ExceptionHandler(TimeSeriesValidateException.class) + public ResponseExtended handleTimeSeriesValidateException(Throwable e) { + return handleException(ErrorConstants.TIME_SERIES_VALIDATE_ERROR, e.getMessage()); + } + + @ExceptionHandler(ForecastValidateException.class) + public ResponseExtended handleForecastValidateException(Throwable e) { + return handleException(ErrorConstants.FORECAST_PARAMS_ERROR, e.getMessage()); + } +} diff --git a/src/main/java/ru/ulstu/controllers/TimeSeriesController.java b/src/main/java/ru/ulstu/controllers/TimeSeriesController.java index da0884f..835cbea 100644 --- a/src/main/java/ru/ulstu/controllers/TimeSeriesController.java +++ b/src/main/java/ru/ulstu/controllers/TimeSeriesController.java @@ -11,8 +11,9 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import ru.ulstu.configurations.ApiConfiguration; import ru.ulstu.models.Forecast; -import ru.ulstu.models.ForecastRequestParams; +import ru.ulstu.models.ForecastParams; import ru.ulstu.models.TimeSeries; +import ru.ulstu.models.exceptions.ModelingException; import ru.ulstu.services.TimeSeriesService; @RestController @@ -37,10 +38,22 @@ public class TimeSeriesController { return new ResponseEntity<>(timeSeriesService.getRandomTimeSeries(length), HttpStatus.OK); } + @GetMapping("getFromString") + @ApiOperation("Преобразовать строку с разделителями во временной ряд") + public ResponseEntity getTimeSeriesFromString(@RequestParam("tsString") String tsString) { + return new ResponseEntity<>(timeSeriesService.getTimeSeriesFromString(tsString), HttpStatus.OK); + } + + @PostMapping("timeSeriesToString") + @ApiOperation("Преобразовать временной ряд в строку с разделителями") + public ResponseEntity getTimeSeriesToString(@RequestBody TimeSeries timeSeries) { + return new ResponseEntity<>(timeSeriesService.getTimeSeriesToString(timeSeries), HttpStatus.OK); + } + @PostMapping("getForecast") @ApiOperation("Получить прогноз временного ряда") - public ResponseEntity getForecastTimeSeries(@RequestBody ForecastRequestParams forecastRequestParams) { - return new ResponseEntity<>(timeSeriesService.getForecast(forecastRequestParams.getOriginalTimeSeries(), - forecastRequestParams.getCountForecast()), HttpStatus.OK); + public ResponseEntity getForecastTimeSeries(@RequestBody ForecastParams forecastParams) throws ModelingException { + return new ResponseEntity<>(timeSeriesService.getForecast(forecastParams.getOriginalTimeSeries(), + forecastParams.getCountForecast()), HttpStatus.OK); } } diff --git a/src/main/java/ru/ulstu/models/Forecast.java b/src/main/java/ru/ulstu/models/Forecast.java index c3413fc..383e8fc 100644 --- a/src/main/java/ru/ulstu/models/Forecast.java +++ b/src/main/java/ru/ulstu/models/Forecast.java @@ -1,23 +1,31 @@ package ru.ulstu.models; public class Forecast { - private TimeSeries originalTimeSeries; + private Model model; private TimeSeries forecast; - public Forecast(TimeSeries originalTimeSeries) { - this.originalTimeSeries = originalTimeSeries; - this.forecast = new TimeSeries("Forecast time series of '" + originalTimeSeries.getName() + "'"); + public Forecast(Model model) { + this.model = model; + this.forecast = new TimeSeries("Forecast time series of '" + model.getOriginalTimeSeries().getName() + "'"); } - public TimeSeries getOriginalTimeSeries() { - return originalTimeSeries; + public Model getModel() { + return model; } - public TimeSeries getForecast() { + public TimeSeries getForecastTimeSeries() { return forecast; } public void addValue(TimeSeriesValue timeSeriesValue) { forecast.addValue(timeSeriesValue); } + + @Override + public String toString() { + return "Forecast{" + + "model=" + model + + ", forecast=" + forecast + + '}'; + } } diff --git a/src/main/java/ru/ulstu/models/ForecastRequestParams.java b/src/main/java/ru/ulstu/models/ForecastParams.java similarity index 93% rename from src/main/java/ru/ulstu/models/ForecastRequestParams.java rename to src/main/java/ru/ulstu/models/ForecastParams.java index 3ec6f85..2375ac6 100644 --- a/src/main/java/ru/ulstu/models/ForecastRequestParams.java +++ b/src/main/java/ru/ulstu/models/ForecastParams.java @@ -1,6 +1,6 @@ package ru.ulstu.models; -public class ForecastRequestParams { +public class ForecastParams { private TimeSeries originalTimeSeries; private int countForecast; diff --git a/src/main/java/ru/ulstu/models/Model.java b/src/main/java/ru/ulstu/models/Model.java new file mode 100644 index 0000000..534e112 --- /dev/null +++ b/src/main/java/ru/ulstu/models/Model.java @@ -0,0 +1,27 @@ +package ru.ulstu.models; + +public class Model { + private TimeSeries originalTimeSeries; + private TimeSeries modelTimeSeries; + + public Model(TimeSeries originalTimeSeries) { + this.originalTimeSeries = originalTimeSeries; + this.modelTimeSeries = new TimeSeries("Model time series of '" + originalTimeSeries.getName() + "'"); + } + + public TimeSeries getOriginalTimeSeries() { + return originalTimeSeries; + } + + public TimeSeries getModelTimeSeries() { + return modelTimeSeries; + } + + public void addValue(TimeSeriesValue timeSeriesValue) { + modelTimeSeries.addValue(timeSeriesValue); + } + + public void addValue(TimeSeriesValue basedOnValue, double value) { + modelTimeSeries.getValues().add(new TimeSeriesValue(basedOnValue.getDate(), value)); + } +} diff --git a/src/main/java/ru/ulstu/models/TimeSeries.java b/src/main/java/ru/ulstu/models/TimeSeries.java index 7515cf3..68d0b1f 100644 --- a/src/main/java/ru/ulstu/models/TimeSeries.java +++ b/src/main/java/ru/ulstu/models/TimeSeries.java @@ -24,6 +24,10 @@ public class TimeSeries { } + public TimeSeries(List values) { + this.values = values; + } + public List getValues() { return values; } @@ -59,4 +63,26 @@ public class TimeSeries { public int getLength() { return values.size(); } + + public TimeSeriesValue getFirstValue() { + if ((values.size() > 0)) { + return values.get(0); + } + throw new RuntimeException("Временной ряд пуст"); + } + + public Double getNumericValue(int t) { + if ((values.size() > t) && (t >= 0)) { + return values.get(t).getValue(); + } + throw new RuntimeException("Индекс выходит за границы временного ряда"); + } + + @Override + public String toString() { + return "TimeSeries{" + + "values=" + values + + ", name='" + name + '\'' + + '}'; + } } diff --git a/src/main/java/ru/ulstu/models/TimeSeriesValue.java b/src/main/java/ru/ulstu/models/TimeSeriesValue.java index 4fe4c0e..838a925 100644 --- a/src/main/java/ru/ulstu/models/TimeSeriesValue.java +++ b/src/main/java/ru/ulstu/models/TimeSeriesValue.java @@ -3,23 +3,32 @@ package ru.ulstu.models; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Objects; public class TimeSeriesValue { - private LocalDate date; + private LocalDateTime date; private Double value; @JsonCreator - public TimeSeriesValue(@JsonProperty(value="date") LocalDate date, @JsonProperty(value = "value") Double value) { + public TimeSeriesValue(@JsonProperty(value = "date") LocalDateTime date, @JsonProperty(value = "value") Double value) { this.date = date; this.value = value; } - public LocalDate getDate() { + public TimeSeriesValue(LocalDateTime date) { + this.date = date; + } + + public TimeSeriesValue(Double value) { + this.value = value; + } + + public LocalDateTime getDate() { return date; } - public void setDate(LocalDate date) { + public void setDate(LocalDateTime date) { this.date = date; } @@ -30,4 +39,23 @@ public class TimeSeriesValue { public void setValue(Double value) { this.value = value; } + + @Override + public String toString() { + return value.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TimeSeriesValue that = (TimeSeriesValue) o; + return Objects.equals(date, that.date) && + Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(date, value); + } } diff --git a/src/main/java/ru/ulstu/models/exceptions/ForecastValidateException.java b/src/main/java/ru/ulstu/models/exceptions/ForecastValidateException.java new file mode 100644 index 0000000..083ef0e --- /dev/null +++ b/src/main/java/ru/ulstu/models/exceptions/ForecastValidateException.java @@ -0,0 +1,7 @@ +package ru.ulstu.models.exceptions; + +public class ForecastValidateException extends ModelingException { + public ForecastValidateException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/ulstu/models/exceptions/ModelingException.java b/src/main/java/ru/ulstu/models/exceptions/ModelingException.java new file mode 100644 index 0000000..4bebd12 --- /dev/null +++ b/src/main/java/ru/ulstu/models/exceptions/ModelingException.java @@ -0,0 +1,7 @@ +package ru.ulstu.models.exceptions; + +public class ModelingException extends Exception { + public ModelingException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/ulstu/models/exceptions/TimeSeriesValidateException.java b/src/main/java/ru/ulstu/models/exceptions/TimeSeriesValidateException.java new file mode 100644 index 0000000..1137ed7 --- /dev/null +++ b/src/main/java/ru/ulstu/models/exceptions/TimeSeriesValidateException.java @@ -0,0 +1,7 @@ +package ru.ulstu.models.exceptions; + +public class TimeSeriesValidateException extends ModelingException { + public TimeSeriesValidateException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/ulstu/models/response/ControllerResponse.java b/src/main/java/ru/ulstu/models/response/ControllerResponse.java new file mode 100644 index 0000000..fa3d13a --- /dev/null +++ b/src/main/java/ru/ulstu/models/response/ControllerResponse.java @@ -0,0 +1,24 @@ +package ru.ulstu.models.response; + +class ControllerResponse { + private final D data; + private final ControllerResponseError error; + + ControllerResponse(D data) { + this.data = data; + this.error = null; + } + + ControllerResponse(ControllerResponseError error) { + this.data = null; + this.error = error; + } + + public D getData() { + return data; + } + + public ControllerResponseError getError() { + return error; + } +} diff --git a/src/main/java/ru/ulstu/models/response/ControllerResponseError.java b/src/main/java/ru/ulstu/models/response/ControllerResponseError.java new file mode 100644 index 0000000..b4fa31c --- /dev/null +++ b/src/main/java/ru/ulstu/models/response/ControllerResponseError.java @@ -0,0 +1,23 @@ +package ru.ulstu.models.response; + +class ControllerResponseError { + private final ErrorConstants description; + private final D data; + + ControllerResponseError(ErrorConstants description, D data) { + this.description = description; + this.data = data; + } + + public int getCode() { + return description.getCode(); + } + + public String getMessage() { + return description.getMessage(); + } + + public D getData() { + return data; + } +} diff --git a/src/main/java/ru/ulstu/models/response/ErrorConstants.java b/src/main/java/ru/ulstu/models/response/ErrorConstants.java new file mode 100644 index 0000000..44c6bdf --- /dev/null +++ b/src/main/java/ru/ulstu/models/response/ErrorConstants.java @@ -0,0 +1,29 @@ +package ru.ulstu.models.response; + +public enum ErrorConstants { + UNKNOWN(0, "Unknown error"), + TIME_SERIES_VALIDATE_ERROR(10, "Некорректный временной ряд"), + FORECAST_PARAMS_ERROR(11, "Некорректные параметры для прогнозирования"), + HTTP_CLIENT_ERROR(66, "Http client error"); + + private final int code; + private final String message; + + ErrorConstants(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } + + @Override + public String toString() { + return String.format("%d: %s", code, message); + } +} diff --git a/src/main/java/ru/ulstu/models/response/Response.java b/src/main/java/ru/ulstu/models/response/Response.java new file mode 100644 index 0000000..fa5ba67 --- /dev/null +++ b/src/main/java/ru/ulstu/models/response/Response.java @@ -0,0 +1,15 @@ +package ru.ulstu.models.response; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class Response extends ResponseEntity { + + public Response(D data) { + super(new ControllerResponse(data), HttpStatus.OK); + } + + public Response(ErrorConstants error) { + super(new ControllerResponse(new ControllerResponseError<>(error, null)), HttpStatus.OK); + } +} diff --git a/src/main/java/ru/ulstu/models/response/ResponseExtended.java b/src/main/java/ru/ulstu/models/response/ResponseExtended.java new file mode 100644 index 0000000..c79be44 --- /dev/null +++ b/src/main/java/ru/ulstu/models/response/ResponseExtended.java @@ -0,0 +1,11 @@ +package ru.ulstu.models.response; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class ResponseExtended extends ResponseEntity { + + public ResponseExtended(ErrorConstants error, E errorData) { + super(new ControllerResponse(new ControllerResponseError(error, errorData)), HttpStatus.OK); + } +} diff --git a/src/main/java/ru/ulstu/services/TimeSeriesService.java b/src/main/java/ru/ulstu/services/TimeSeriesService.java index 668e547..78a8e03 100644 --- a/src/main/java/ru/ulstu/services/TimeSeriesService.java +++ b/src/main/java/ru/ulstu/services/TimeSeriesService.java @@ -1,19 +1,28 @@ package ru.ulstu.services; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import ru.ulstu.TimeSeriesUtils; import ru.ulstu.models.Forecast; import ru.ulstu.models.TimeSeries; import ru.ulstu.models.TimeSeriesValue; +import ru.ulstu.models.exceptions.ModelingException; import ru.ulstu.tsMethods.exponential.NoTrendNoSeason; -import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; @Service public class TimeSeriesService { + private static final Logger LOGGER = LoggerFactory.getLogger(TimeSeriesService.class); + public TimeSeries getRandomTimeSeries(int length) { TimeSeries ts = new TimeSeries("Random time series"); - LocalDate dateStart = LocalDate.now().minusDays(length); + LocalDateTime dateStart = LocalDateTime.now().minusDays(length); for (int i = 0; i < length; i++) { ts.getValues().add(new TimeSeriesValue(dateStart, Math.random())); dateStart = dateStart.plusDays(1); @@ -21,10 +30,35 @@ public class TimeSeriesService { return ts; } - public Forecast getForecast(TimeSeries timeSeries, int countForecast) { - NoTrendNoSeason nn = new NoTrendNoSeason(timeSeries, countForecast); - nn.setAlpa(0.1); - nn.createModel(); - return nn.getForecastTimeSeries(); + public Forecast getForecast(TimeSeries timeSeries, int countPoints) throws ModelingException { + NoTrendNoSeason nn = new NoTrendNoSeason(0.8); + return nn.getForecast(timeSeries, countPoints); + } + + public TimeSeries getTimeSeriesFromString(String tsString) { + List tsValues = Arrays.stream(tsString.split("\n")) + .flatMap(v -> Arrays.stream(v.split(";"))) + .flatMap(v -> Arrays.stream(v.split(","))) + .flatMap(v -> Arrays.stream(v.split("
"))) + .filter(v -> { + try { + Double.parseDouble(v); + return true; + } catch (NumberFormatException e) { + return false; + } + }) + .map(Double::parseDouble) + .map(TimeSeriesValue::new) + .collect(Collectors.toList()); + return TimeSeriesUtils.fillDates(new TimeSeries(tsValues)); + } + + public String getTimeSeriesToString(TimeSeries timeSeries) { + return timeSeries + .getValues() + .stream() + .map(v -> v.getValue().toString().replaceAll("\\.", ",")) + .collect(Collectors.joining(";")); } } diff --git a/src/main/java/ru/ulstu/tsMethods/TimeSeriesMethod.java b/src/main/java/ru/ulstu/tsMethods/TimeSeriesMethod.java index dc86a73..a6a055c 100644 --- a/src/main/java/ru/ulstu/tsMethods/TimeSeriesMethod.java +++ b/src/main/java/ru/ulstu/tsMethods/TimeSeriesMethod.java @@ -1,34 +1,111 @@ package ru.ulstu.tsMethods; +import ru.ulstu.TimeSeriesUtils; import ru.ulstu.models.Forecast; +import ru.ulstu.models.Model; import ru.ulstu.models.TimeSeries; +import ru.ulstu.models.TimeSeriesValue; +import ru.ulstu.models.exceptions.ForecastValidateException; +import ru.ulstu.models.exceptions.ModelingException; +import ru.ulstu.models.exceptions.TimeSeriesValidateException; -import java.util.HashMap; -import java.util.Map; +import java.time.temporal.ChronoUnit; +/** + * Наиболее общая логика моделировани и прогнозирования временных рядов + */ public abstract class TimeSeriesMethod { - protected TimeSeries originalTimeSeries; - protected TimeSeries modelTimeSeries; - protected Forecast forecastTimeSeries; - protected int countForecast; - protected Map parameters = new HashMap<>(); - - protected void init() { - modelTimeSeries = new TimeSeries("Model time series of '" + originalTimeSeries.getName() + "'"); - forecastTimeSeries = new Forecast(originalTimeSeries); + /** + * Возвращает модельное представление временного ряда: для тех же точек времени что и в параметре timeSeries + * строится модель. Количество точек может быть изменено: сокращено при сжатии ряда, увеличено при интерполяции. + * Метод является шаблонным, выполняет операции валидации исходного ряда и потом его моделирование + * + * @param timeSeries исходный временной ряд подлежащий моделированию + * @return модель временного ряда + * @throws TimeSeriesValidateException + */ + public Model getModel(TimeSeries timeSeries) throws TimeSeriesValidateException { + validateTimeSeries(timeSeries); + return getModelOfValidTimeSeries(timeSeries); } - public TimeSeries getOriginalTimeSeries() { - return originalTimeSeries; + /** + * Возвращает модельное представление валидного временного ряда: для тех же точек времени что и в параметре timeSeries + * строится модель. Количество точек может быть изменено: сокращено при сжатии ряда, увеличено при интерполяции. + * + * @param timeSeries исходный временной ряд подлежащий моделированию + * @return + */ + protected abstract Model getModelOfValidTimeSeries(TimeSeries timeSeries); + + /** + * Выполняет построение прогноза временного ряда. Даты спрогнозированных точек будут сгенерированы по модельным точкам. + * + * @param model модель временного ряда + * @param countPoints количество точек для прогнозирования + * @return прогноз временного ряда + */ + public Forecast getForecast(Model model, int countPoints) throws ModelingException { + Forecast forecast = new Forecast(model); + forecast = generateEmptyForecastPoints(forecast, countPoints); + forecast = makeForecast(forecast); + if (!forecast.getForecastTimeSeries().getFirstValue() + .equals(forecast.getModel().getModelTimeSeries().getLastValue())) { + throw new ForecastValidateException("Первая точка прогноза должна совпадать с последней модельной точкой"); + } + return forecast; } - public TimeSeries getModelTimeSeries() { - return modelTimeSeries; + /** + * Выполняет построение прогноза для уже сгенерированных будущих точек временного ряда. + * + * @param forecast Заготовка прогноза временного ряда с пустыми значениями + * @return + */ + protected abstract Forecast makeForecast(Forecast forecast); + + protected Forecast generateEmptyForecastPoints(Forecast forecast, int countPointForecast) { + long diffMilliseconds = TimeSeriesUtils.getTimeDifferenceInMilliseconds(forecast.getModel().getOriginalTimeSeries()); + forecast.getForecastTimeSeries() + .addValue(new TimeSeriesValue(forecast.getModel().getModelTimeSeries().getLastValue().getDate())); + for (int i = 1; i < countPointForecast + 1; i++) { + forecast.getForecastTimeSeries() + .addValue(new TimeSeriesValue(forecast.getForecastTimeSeries().getValues().get(i - 1).getDate().plus(diffMilliseconds, ChronoUnit.MILLIS))); + } + return forecast; } - public Forecast getForecastTimeSeries() { - return forecastTimeSeries; + /** + * Выполняет построение модели и прогноза временного ряда. Даты спрогнозированных точек будут сгенерированы + * по модельным точкам. + * + * @param timeSeries временной ряда + * @param countPoints количество точек для прогнозирования + * @return прогноз временного ряда + */ + public Forecast getForecast(TimeSeries timeSeries, int countPoints) throws ModelingException { + validateForecastParams(countPoints); + return getForecast(getModel(timeSeries), countPoints); } - public abstract void createModel(); + private void validateForecastParams(int countPoints) throws ForecastValidateException { + if (countPoints < 1) { + throw new ForecastValidateException("Количество прогнозных точек должно быть больше 0"); + } + } + + private void validateTimeSeries(TimeSeries timeSeries) throws TimeSeriesValidateException { + if (timeSeries == null || timeSeries.isEmpty()) { + throw new TimeSeriesValidateException("Временной ряд должен быть не пустым"); + } + if (timeSeries.getLength() < 2) { + throw new TimeSeriesValidateException("Временной ряд должен содержать хотя бы 2 точки"); + } + if (timeSeries.getValues().stream().anyMatch(val -> val == null || val.getValue() == null)) { + throw new TimeSeriesValidateException("Временной ряд содержит пустые значения"); + } + if (timeSeries.getValues().stream().anyMatch(val -> val.getDate() == null)) { + throw new TimeSeriesValidateException("Временной ряд должен иметь отметки времени"); + } + } } diff --git a/src/main/java/ru/ulstu/tsMethods/exponential/NoTrendNoSeason.java b/src/main/java/ru/ulstu/tsMethods/exponential/NoTrendNoSeason.java index be15776..deca8ef 100644 --- a/src/main/java/ru/ulstu/tsMethods/exponential/NoTrendNoSeason.java +++ b/src/main/java/ru/ulstu/tsMethods/exponential/NoTrendNoSeason.java @@ -1,45 +1,41 @@ package ru.ulstu.tsMethods.exponential; +import ru.ulstu.models.Forecast; +import ru.ulstu.models.Model; import ru.ulstu.models.TimeSeries; -import ru.ulstu.tsMethods.Param; import ru.ulstu.tsMethods.TimeSeriesMethod; public class NoTrendNoSeason extends TimeSeriesMethod { - public NoTrendNoSeason(TimeSeries originalTimeSeries, int countForecast) { - this.originalTimeSeries = originalTimeSeries; - this.countForecast = countForecast; - if (originalTimeSeries.isEmpty()) { - throw new RuntimeException("Time series must not empty"); - } + private double alpha; + + public NoTrendNoSeason(double alpha) { + this.alpha = alpha; } @Override - protected void init() { - super.init(); - modelTimeSeries.addValue(originalTimeSeries.getValues().get(0)); - forecastTimeSeries.addValue(originalTimeSeries.getValues().get(0)); - parameters.put(Param.ALPHA, 1.0); - } - - public void setAlpa(double value) { - parameters.put(Param.ALPHA, value); + protected Model getModelOfValidTimeSeries(TimeSeries timeSeries) { + Model model = new Model(timeSeries); + model.addValue(timeSeries.getFirstValue()); + //выполняется проход модели по сглаживанию + for (int t = 1; t < timeSeries.getValues().size(); t++) { + model.addValue(timeSeries.getValues().get(t), + (1 - alpha) * timeSeries.getNumericValue(t) + + alpha * model.getModelTimeSeries().getValues().get(t - 1).getValue()); + } + return model; } @Override - public void createModel() { - init(); - double e; - - //выполняется проход модели по сглаживанию и прогнозированию countPointForecast точек - for (int t = 0; t < originalTimeSeries.getValues().size() - 1 + countForecast; t++) { - // пока не дошли до конца ряда - сглаживаем, иначе строим прогноз - if (t < originalTimeSeries.getValues().size()) { - e = originalTimeSeries.getValues().get(t).getValue() - forecastTimeSeries.getForecast().getValues().get(t).getValue(); - } else { - e = 0; - } - modelTimeSeries.addValue(modelTimeSeries.getLastValue(), modelTimeSeries.getValues().get(t).getValue() + parameters.get(Param.ALPHA) * e); // уровень - forecastTimeSeries.addValue(modelTimeSeries.getLastValue()); // прогноз + protected Forecast makeForecast(Forecast forecast) { + forecast.getForecastTimeSeries() + .getFirstValue() + .setValue(forecast.getModel().getModelTimeSeries().getLastValue().getValue()); + for (int t = 1; t < forecast.getForecastTimeSeries().getLength(); t++) { + forecast.getForecastTimeSeries() + .getValues() + .get(t) + .setValue(forecast.getForecastTimeSeries().getValues().get(t - 1).getValue()); } + return forecast; } }