Найти решение в цикле с датами
В домашнем задании мне нужно вытащить курс рубля к доллару с интернет-ресурса API Центробанка по всем датам одного месяца и найти, между какими двумя соседними датами он максимально изменился.
Я объявил массив ArrayList с датами месяца, для примера взял март 2023 г.
Далее написал последовательно два цикла for для массивов.
Первый цикл вытаскивает курс рубля к доллару с API Центробанка на каждую дату месяца, записывает его напротив соответствующей даты и выводит результат в консоль вот таким образом:
Курс на 01/03/2023 74.8932
Курс на 02/03/2023 75.2513
. . .
. . .
. . .
Курс на 31/03/2023 77.0863
Также в этом цикле курс рубля складывается в массив ArrayList listCourses и во втором цикле этот массив перебирается.
Второй цикл перебирает упомянутый массив ArrayLis listCourses с курсами и находит максимальную и минимальную разницу между двумя соседними элементами массива, т.е. между двумя соседними курсами.
И выводит результат на экран вот в таком виде:
Максимальный рост между двумя соседними элементами (т.е. рост курса между двумя соседними датами): 0,663
Максимальное снижение между двумя соседними элементами (т.е. снижение курса между двумя соседними датами): -0,648
Мне нужно, чтобы по результату второго цикла выводилась не только максимальная и минимальная разница между соседними элементами ArrayList, но и соответствующая второму из этих соседних элементов дата из первого цикла.
Т.е. как-то вот так:
Максимальный рост между двумя соседними элементами: 0,663 соответствует дате 15/03/2023
Максимальное снижение между двумя соседними элементами: -0,648 соответствует дате 31/03/2023
Если кто-нибудь сможет подсказать идею, буду очень благодарен.
Вот мой код:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.RoundingMode;
import java.net.URL;
import java.net.URLConnection;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ExchangeRates {
public static void main(String[] args) throws IOException {
// Скачиваем содержимое исходной страницы API Центробанка.
String originalPage = downloadWebPage("https://cbr.ru/scripts/XML_dynamic.asp?date_req1=12/11/2021&date_req2=12/11/2021&VAL_NM_RQ=R01235");
// Задаём адрес исходной страницы API Центробанка в текстовом формате.
String originalPageText = "https://cbr.ru/scripts/XML_dynamic.asp?date_req1=12/11/2021&date_req2=12/11/2021&VAL_NM_RQ=R01235";
// Создаем массив ArrayList, с датами месяца.
List<String> listDates = Arrays.asList("01/03/2023", "02/03/2023", "03/03/2023", "04/03/2023", "05/03/2023", "06/03/2023", "07/03/2023", "08/03/2023", "09/03/2023", "10/03/2023", "11/03/2023", "12/03/2023", "13/03/2023", "14/03/2023", "15/03/2023", "16/03/2023", "17/03/2023", "18/03/2023", "19/03/2023", "20/03/2023", "21/03/2023","22/03/2023", "23/03/2023", "24/03/2023", "25/03/2023", "26/03/2023", "27/03/2023", "28/03/2023", "29/03/2023", "30/03/2023", "31/03/2023");
// Создаем массив ArrayList, куда записываем в качестве элементов курс рубля на текущую дату.
List<Double> listCourses = new ArrayList<>();
for (int i = 0; i < listDates.size(); i++) {
String dtStr = listDates.get(i);
int startIndex = originalPage.lastIndexOf("<Value>") + 7;
int endIndex = originalPage.lastIndexOf("</Value>");
String nextDate;
// Меняем в адресе исходной страницы дату на следующую.
String urlWithNextDate = originalPageText.replaceAll("12/11/2021", dtStr);
String nextPage = downloadWebPage(urlWithNextDate);
if (nextPage.contains("<Value>")) {
String courseNextPage = nextPage.substring(startIndex, endIndex);
// Задаём курс в виде переменной Double.
double courseNextDoble = Double.parseDouble(courseNextPage.replace(",", "."));
// System.out.println("Курс в типе переменной Double:");
// System.out.println(courseNextDoble);
// Выводим на экран дату и соответствующий курс.
System.out.println("Курс на " + dtStr + " " + courseNextDoble);
listCourses.add(courseNextDoble);
} else {
String courseNextPage = "";
System.out.println("Курс на " + dtStr);
}
}
//Далее ищем максимальные перепады курса.
double max = maxDifference(listCourses);
double min = minDifference(listCourses);
DecimalFormat df = new DecimalFormat("0.000");
df.setRoundingMode(RoundingMode.DOWN);
System.out.println("\nМаксимальный рост между двумя соседними элементами: " + df.format(max));
System.out.println("Максимальное снижение между двумя соседними элементами: " + df.format(min));
}
//Пишем классы для поиска максимальных перепадов курса.
//Сначала максимальную разницу находим.
public static double maxDifference(List<Double> listCourses) {
if (listCourses == null || listCourses.size() == 0) {
return Double.MIN_VALUE;
}
int len = listCourses.size();
double[] diff = new double[len - 1];
for (int i = 0; i < len - 1; i++) {
diff[i] = listCourses.get(i + 1) - listCourses.get(i);
}
return max(diff);
}
public static double max(double[] diff) {
if (diff == null || diff.length == 0) {
return Double.MIN_VALUE;
}
double max = diff[0];
for (int i = 0, len = diff.length; i < len; i++) {
//not necessary,since 'int[] data' is sorted,so 'int[] diff' is progressively increased.
//int tmp=diff[i]>0?diff[i]:(-diff[i]);
if (max < diff[i]) {
max = diff[i];
}
}
return max;
}
//Теперь минимальную разницу находим.
public static double minDifference(List<Double> listCourses) {
if (listCourses == null || listCourses.size() == 0) {
return Double.MIN_VALUE;
}
int len = listCourses.size();
double[] diff = new double[len - 1];
for (int i = 0; i < len - 1; i++) {
diff[i] = listCourses.get(i + 1) - listCourses.get(i);
}
return min(diff);
}
public static double min(double[] diff) {
if (diff == null || diff.length == 0) {
return Double.MIN_VALUE;
}
double min = diff[0];
for (int i = 0, len = diff.length; i < len; i++) {
//not necessary,since 'int[] data' is sorted,so 'int[] diff' is progressively increased.
//int tmp=diff[i]>0?diff[i]:(-diff[i]);
if (min > diff[i]) {
min = diff[i];
}
}
return min;
}
private static String downloadWebPage(String url) throws IOException {
StringBuilder result = new StringBuilder();
String line;
URLConnection urlConnection = new URL(url).openConnection();
try (InputStream is = urlConnection.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
while ((line = br.readLine()) != null) {
result.append(line);
}
}
return result.toString();
}
}
Ответы (2 шт):
Идея в том, что курс никак не привязан к дате. Он просто сохраняется в списке listCourses без даты, ну и дальше его вытащить никак не возможно, потому что область видимости переменной dateStr находится в пределах первого цикла.
Вот что можно почитать про область видимости переменных: Объясните про область видимости и использование переменных
Видимость переменных/классов определяется модификаторами доступа (private, public etc), а также областями видимости внутри блоков кода, ограниченных фигурными скобками {}
В случае с последними переменная объявленная в к-л блоке кода, окружённая {} видна только внутри этого блока (за исключением случаев, когда это переменная класса, имеющая public или default(т.е. без модификаотора; видна в одном пакете) уровень доступа) и всех внутренних блоках.
Попробуйте привязать дату к курсу, с помощью Map или создать объект из двух полей, дата и курс и добавлять его в список. Во втором цикле можно вытащить дату вместе с курсом из списка или Map. Вот такая идея, думаю с помощью этого можно легко написать код. Поскольку это учебное задание то придется писать самому.
Для начала следует правильно прочитать и распарсить XML вида для заданного URL, в частности, чтобы вычислить изменения котировок в первый день месяца, начальная дата должна быть на день раньше (28.02.2023):
<ValCurs ID="R01235" DateRange1="28.02.2023" DateRange2="31.03.2023" name="Foreign Currency Market Dynamic">
<Record Date="28.02.2023" Id="R01235">
<Nominal>1</Nominal>
<Value>75,4323</Value>
</Record>
<Record Date="01.03.2023" Id="R01235">
<Nominal>1</Nominal>
<Value>74,8932</Value>
</Record>
<Record Date="02.03.2023" Id="R01235">
<Nominal>1</Nominal>
<Value>75,2513</Value>
</Record>
<!-- ... -->
</ValCurs>
в коллекцию экземпляров котировок List<CurrencyRate>, где класс CurrencyRate содержит поля даты котировки (значение атрибута Date) и её значения в элементе Value.
Для чтения XML данных рекомендуется использовать библиотеку Jackson.
Также в представленном решении используется проект Lombok.
Фрагмент pom.xml файла:
<!-- pom.xml -->
<properties>
<jackson.version>2.13.4</jackson.version>
<lombok.version>1.18.10</lombok.version>
</properties>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!-- ... -->
</dependencies>
<!-- ... -->
- Пример POJO для чтения корневого элемента в исходном XML:
@Data
@JacksonXmlRootElement(localName = "ValCurs")
public class ValCurs {
@JacksonXmlProperty(isAttribute = true, localName = "ID")
private String id;
@JacksonXmlProperty(isAttribute = true, localName = "DateRange1")
@JsonFormat(pattern = "dd.MM.yyyy")
private LocalDate from;
@JacksonXmlProperty(isAttribute = true, localName = "DateRange2")
@JsonFormat(pattern = "dd.MM.yyyy")
private LocalDate to;
@JacksonXmlProperty(isAttribute = true, localName = "name")
private String name;
@JacksonXmlProperty(localName = "Record")
@JacksonXmlElementWrapper(useWrapping = false)
private List<CurrencyRate> rates;
}
- Пример POJO для котировки, включая десериализатор для чисел с запятой в качестве десятичного разделителя (излишние поля игнорируются).
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
class CurrencyRate {
@JacksonXmlProperty(isAttribute = true, localName = "Date")
@JsonFormat(pattern = "dd.MM.yyyy")
private LocalDate date;
@JsonProperty("Value")
@JsonDeserialize(using = MyDoubleDeserializer.class, as = Double.class)
private double value;
}
- Класс-десериализатор, использующий
NumberFormatдля немецкой локали:
public class MyDoubleDeserializer extends JsonDeserializer<Double> {
private static final NumberFormat nf = NumberFormat.getInstance(Locale.GERMANY);
@SneakyThrows
@Override
public Double deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) {
return nf.parse(jsonParser.getText()).doubleValue();
}
}
Тогда метод для чтения котировок в заданном месяце может быть реализован так:
@SneakyThrows
public static ValCurs readRates(YearMonth month) {
LocalDate from = month.atDay(1).minusDays(1);
LocalDate to = month.atEndOfMonth();
DateTimeFormatter ddMMyyyy = DateTimeFormatter.ofPattern("dd/MM/yyyy");
String url = String.format(
"https://cbr.ru/scripts/XML_dynamic.asp?date_req1=%s&date_req2=%s&VAL_NM_RQ=R01235",
from.format(ddMMyyyy),
to.format(ddMMyyyy)
);
XmlMapper xmlMapper = new XmlMapper();
xmlMapper.registerModule(new JavaTimeModule());
return xmlMapper.readValue(new URL(url), ValCurs.class);
}
После получения качественных данных, требуемые значения изменений достаточно просто определить:
public static void findMaxChanges(List<CurrencyRate> rates) {
CurrencyRate prev = rates.get(0);
CurrencyRate maxPlus = null;
CurrencyRate maxMinus = null;
double max = 0.0, min = 0.0, sum = 0.0;
for (int i = 1, n = rates.size(); i < n; i++) {
CurrencyRate curr = rates.get(i);
double change = curr.getValue() - prev.getValue();
if (change > 0) {
if (maxPlus == null || change > max) {
max = change;
maxPlus = curr;
}
} else if (change < 0) {
if (maxMinus == null || change < min) {
min = change;
maxMinus = curr;
}
}
sum += change;
prev = curr;
}
if (maxPlus != null) {
System.out.printf("Максимальный прирост: %.4f соответствует дате %s%n", max, maxPlus.getDate());
}
if (maxMinus != null) {
System.out.printf("Максимальное снижение: %.4f соответствует дате %s%n", min, maxMinus.getDate());
}
System.out.printf("Суммарное изменение за месяц: %.4f%n", sum);
System.out.printf("Среднее ежедневное изменение за месяц: %.4f для %d котировок%n", sum / (rates.size() - 1), rates.size() - 1);
}
Тест:
// прочитать котировки
ValCurs valCurs = readRates(YearMonth.of(2023, 3));//xmlMapper.readValue(XML, ValCurs.class);
// вывести первых пять записей
valCurs.getRates().stream().limit(5).forEach(System.out::println);
// найти максимальные изменения
findMaxChanges(valCurs.getRates());
Результаты:
CurrencyRate(date=2023-02-28, value=75.4323)
CurrencyRate(date=2023-03-01, value=74.8932)
CurrencyRate(date=2023-03-02, value=75.2513)
CurrencyRate(date=2023-03-03, value=75.4729)
CurrencyRate(date=2023-03-04, value=75.4592)
Максимальный прирост: 0.6638 соответствует дате 2023-03-17
Максимальное снижение: -0.6489 соответствует дате 2023-03-24
Суммарное изменение за месяц: 1.6540
Среднее ежедневное изменение за месяц: 0.0752 для 22 котировок