Курс рубля к доллару с интернет-ресурса API Центробанка по всем датам одного месяца

Рейтинг: 6Ответов: 2Опубликовано: 24.04.2023

В домашнем задании мне нужно вытащить курс рубля к доллару с интернет-ресурса 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.
            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();
    }
}

Ответы

▲ 3Принят

Для начала следует правильно прочитать и распарсить 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 котировок
▲ 2

Идея в том, что курс никак не привязан к дате. Он просто сохраняется в списке listCourses без даты, ну и дальше его вытащить никак не возможно, потому что область видимости переменной dateStr находится в пределах первого цикла.

Вот что можно почитать про область видимости переменных: Объясните про область видимости и использование переменных

Видимость переменных/классов определяется модификаторами доступа (private, public etc), а также областями видимости внутри блоков кода, ограниченных фигурными скобками {}

В случае с последними переменная объявленная в к-л блоке кода, окружённая {} видна только внутри этого блока (за исключением случаев, когда это переменная класса, имеющая public или default(т.е. без модификаотора; видна в одном пакете) уровень доступа) и всех внутренних блоках.


Попробуйте привязать дату к курсу, с помощью Map или создать объект из двух полей, дата и курс и добавлять его в список. Во втором цикле можно вытащить дату вместе с курсом из списка или Map. Вот такая идея, думаю с помощью этого можно легко написать код. Поскольку это учебное задание то придется писать самому.