Недавно прошёл конкурс красоты кода от Сбера. Участие по направлению Android в этом конкурсе было интересным опытом, которым я поделюсь в статье.

Содержание:

  • О конкурсе

  • Задание по направлению Android

  • Моё решение

  • Решение chatGPT

  • Звонок другу

  • Комментарий победителя в номинации «Изящный код» и его решение

О конкурсе

Компьютерный код может написать любой разработчик. Красивый код пишут лишь единицы. Чистый, изящный, лаконичный, читаемый и понятный код, который работает без багов — это настоящее произведение искусства в сфере разработки.

Для участников стояла задача написать красивый код по одному из пяти направлений (Python, Java, Data Science, Front-end, Android).

Каждое направление предусматривало 3 номинации:

  • Краса кода — решение, признанное максимально эффективным по мнению жюри;

  • Изящный код — самое лаконичное решение, соответствующее поставленной задаче;

  • Звезда кода — самое неординарное решение по общей оценке жюри.

Призы конкурса: iPhone 14, колонка SberBoom Mini и приглашение на вечеринку в честь дня программиста.

Задание по направлению Android

По направлению Android задание было следующим:

Имея вводные данные, написать функцию, получающую список категорий (List Category), список характеристик (List Feature), и преобразующую их в один List элементов, и возвращающую его.
Правила формирования результирующего списка:
- Первый элемент связан с категорией (Category). Хранит в себе всю информацию о категории.
- Далее идут все элементы, связанные с характеристикой (Feature) относящиеся к данной категории.
- После последней характеристики, относящийся к открытой категории, идет элемент, сигнализирующий о том, что категория закончилась. Хранит в себе только CategoryId.
Количество элементов не ограничено.

Вводные данные:

// Класс категории
data class Category(
    val categoryId: Int,
    val name: String)

// Класс характеристики
data class Feature(
    val featureId: Int,
    val categoryId: Int,
    val title: String,
    var value: Int)

// Список всех доступных категорий можем получить методом
fun getCategories(): List<Category>

// Список характеристик для отображения можем получить методом
fun getFeatures(): List<Feature>

// Получаемые данными методами элементы не отсортированы

// Характеристика и категория связаны между собой полем
// val categoryId: Int

Моё решение

Итак, нам нужно вывести список категорий и характеристик. При этом элементы в списке должны находиться разные типы элементов: категория, характеристика, концевик категории (её ID).

Первое, что пришло на ум — List<Any>. Это плохое решение, ведь так в этот список можно положить вообще всё, что угодно.

Я посмотрел, что объединяет входные дата-классы. Воспользовавшись nullable-свойствами, я создал общий дата-класс. В названии его я буквально написал, что он содержит.

data class CategoryOrFeatureOrEndElement(
    val categoryId: Int,
    val categoryName: String? = null,
    val featureId: Int? = null,
    val featureTitle: String? = null,
    var featureValue: Int? = null
)

Объекты этого класса будут наполнять результирующий список.
Дальше ничего не интересного: я просто воспользовался вложенными циклами.

fun getCategoriesWithFeatures(categories: List<Category>, features: List<Feature>):
        MutableList<CategoryOrFeatureOrEndElement> {
    val categoriesWithFeatures = mutableListOf<CategoryOrFeatureOrEndElement>()

    for (category in categories) {
        categoriesWithFeatures.add(
            CategoryOrFeatureOrEndElement(categoryId = category.categoryId, categoryName = category.name)
        )

        for (feature in features) {
            if (feature.categoryId == category.categoryId) {
                categoriesWithFeatures.add(
                    CategoryOrFeatureOrEndElement(
                        featureId = feature.featureId,
                        categoryId = category.categoryId,
                        featureTitle = feature.title,
                        featureValue = feature.value
                    )
                )
            }
        }

        categoriesWithFeatures.add(
            CategoryOrFeatureOrEndElement(categoryId = category.categoryId)
        )
    }

    return categoriesWithFeatures
}

С помощью именованных аргументов конструктора класса, в список сначала добавляется категория, потом все соответствующие характеристики, затем ID категории. Такое довольно простое решение.

Небольшая оптимизация:

fun getCategoriesWithFeaturesOptimized(categories: List<Category>, features: List<Feature>):
        MutableList<CategoryOrFeatureOrEndElement> {
    val categoriesWithFeatures = mutableListOf<CategoryOrFeatureOrEndElement>()

    val mutableFeatures = features.toMutableList()

    for (category in categories) {
        categoriesWithFeatures.add(
            CategoryOrFeatureOrEndElement(categoryId = category.categoryId, categoryName = category.name)
        )

        val iterator = mutableFeatures.iterator()
        while (iterator.hasNext()) {
            val feature = iterator.next()
            if (feature.categoryId == category.categoryId) {
                categoriesWithFeatures.add(
                    CategoryOrFeatureOrEndElement(
                        featureId = feature.featureId,
                        categoryId = category.categoryId,
                        featureTitle = feature.title,
                        featureValue = feature.value
                    )
                )
                iterator.remove()
            }
        }

        categoriesWithFeatures.add(
            CategoryOrFeatureOrEndElement(categoryId = category.categoryId)
        )
    }

    return categoriesWithFeatures
}

В данном случае после добавления характеристики (feature) в итоговый список (categoriesWithFeatures), она удаляется из списка характеристик (mutableFeatures), поэтому следующая итерация цикла пройдёт быстрее, так как в списке будет меньше элементов.
Однако в данном случае необходимо создавать mutableFeatures (изменяемый дупликат списка features), что может быть неэффективно по памяти, если начальный список достаточно большой.
Эта оптимизация подходила бы, если входная функция возвращала изменяемый список:

fun getFeatures(): MutableList<Feature>

После участия в конкурсе я нашёл конструкции языка и методы, которые позволяют улучшить код. Например, вместо моего дата-класса больше подошёл бы Sealed Class с тремя потомками: Категорией, Характеристикой и ID категории.

Решение chatGPT

Я попросил chatGPT решить поставленную задачу, и вот что он написал:

data class CategoryWithFeatures(
    val categoryId: Int,
    val categoryName: String,
    val features: List<Feature>
)

fun combineCategoriesAndFeatures(categories: List<Category>, features: List<Feature>): List<CategoryWithFeatures> {
    val categoryMap = categories.associateBy { it.categoryId }

    val result = mutableListOf<CategoryWithFeatures>()
    var currentCategory: CategoryWithFeatures? = null

    for (feature in features) {
        if (currentCategory == null || feature.categoryId != currentCategory.categoryId) {
            // Создаем новую категорию, когда текущая категория закончена или еще не начата
            val category = categoryMap[feature.categoryId]
            if (category != null) {
                currentCategory = CategoryWithFeatures(category.categoryId, category.name, mutableListOf())
                result.add(currentCategory)
            }
        }

        // Добавляем характеристику к текущей категории
        currentCategory?.features?.add(feature)
    }

    return result
}

Моё решение лучше этого, потому что оно, как минимум, работает. Ура, моё решение лучше решения chatGPT, плюс самооценка !

Что же делает наш умный товарищ:

  • пытается добавить элемент к неизменяемому списку (строка 24);

  • пишет комментарии в решении, которое должно читаться без комментариев.

Хорошо, давайте закроем на это глаза. Закрыли, но решение всё ещё плохое: оно не соответствует поставленной задаче:

  • Основная функция в итоге возвращает список объектов CategoryWithFeatures. Этот класс содержит данные категории и список характеристик. Всё это он запихал в один класс. Теперь нет отдельных элементов категории, характеристики и ID категории, которые должны быть в результирующем списке по условию задачи.

  • Его цикл работает только, если во входных данных характеристики идут строго по порядку: сначала характеристики одной категории, потом другой. В противном случае в цикле создаются дубликаты категорий.

Вот, что мы в итоге получаем:

// Код всё ещё от chatGPT
fun main() {
    val categories = getCategories()
    val features = getFeatures()

    val combinedList = combineCategoriesAndFeatures(categories, features)

    // Выводим результат
    for (categoryWithFeatures in combinedList) {
        println("Category: ${categoryWithFeatures.categoryName} (ID: ${categoryWithFeatures.categoryId})")
        for (feature in categoryWithFeatures.features) {
            println(" - Feature: ${feature.title} (ID: ${feature.featureId}), Value: ${feature.value}")
        }
    }
}
Входные и выходные данные, дублирование категорий
Входные и выходные данные, дублирование категорий

Я также показал своё решение chatGPT и попросил его улучшить. Вот его версия моего решения:

data class CategoryOrFeatureOrEndElement(
    val categoryId: Int,
    val categoryName: String? = null,
    val featureId: Int? = null,
    val featureTitle: String? = null,
    var featureValue: Int? = null
)

fun groupCategoriesWithFeatures(categories: List<Category>, features: List<Feature>):
        List<CategoryOrFeatureOrEndElement> {
    val groupedFeatures = features.groupBy { it.categoryId }

    val result = mutableListOf<CategoryOrFeatureOrEndElement>()

    for (category in categories) {
        result.add(CategoryOrFeatureOrEndElement(category.categoryId, category.name))

        groupedFeatures[category.categoryId]?.forEach { feature ->
            result.add(CategoryOrFeatureOrEndElement(
                featureId = feature.featureId,
                categoryId = category.categoryId,
                featureTitle = feature.title,
                featureValue = feature.value
            ))
        }

        result.add(CategoryOrFeatureOrEndElement(category.categoryId))
    }

    return result
}

Мой дата-класс он оставил без изменений и поменял саму функцию. Теперь сначала с помощью функции groupBy создаётся Map<Int, List<Feature>>, где ключи — ID категории, значения — списки характеристик. Потом всё те же вложенные циклы, только теперь нужные характеристики не нужно искать, а просто брать из созданного Map.

В итоге:

  • результат работы такой же;

  • вложенные циклы остались;

  • перед вложенными циклами появились: ещё один цикл от функции groupBy и дополнительная переменная;

  • код стал короче, но, как по мне, его читаемость не изменилась.

Звонок другу

Не получив фидбека от организаторов конкурса, я попросил его у друга. Вместе с фидбеком я получил ещё один вариант решения:

// Возврат объекта с несколькими нуллабельными полями в качестве метки — это не очень хорошо
data class CategoryOrFeatureOrEndElement(
    val categoryId: Int,
    val categoryName: String? = null,
    val featureId: Int? = null,
    val featureTitle: String? = null,
    var featureValue: Int? = null
)

// Зачем возвращать MutableList, если можно вернуть просто List?
fun getCategoriesWithFeatures(categories: List<Category>, features: List<Feature>):
        List<CategoryOrFeatureOrEndElement> {
    val categoriesWithFeatures = mutableListOf<CategoryOrFeatureOrEndElement>()

    val featureMap = mutableMapOf<Int, List<CategoryOrFeatureOrEndElement>>()

    for (feature in features) {
        var mutableCategoryFeatures: MutableList<CategoryOrFeatureOrEndElement>? = null
        val categoryFeatures = featureMap[feature.categoryId]
        if (categoryFeatures != null) {
            mutableCategoryFeatures = categoryFeatures.toMutableList()
            mutableCategoryFeatures.add(CategoryOrFeatureOrEndElement(
                categoryId = feature.categoryId,
                featureId = feature.featureId,
                featureTitle = feature.title,
                featureValue = feature.value
            ))
            featureMap[feature.categoryId] = mutableCategoryFeatures
            continue
        }
        mutableCategoryFeatures = mutableListOf()
        mutableCategoryFeatures.add(CategoryOrFeatureOrEndElement(
            categoryId = feature.categoryId,
            featureId = feature.featureId,
            featureTitle = feature.title,
            featureValue = feature.value
        ))
        featureMap[feature.categoryId] = mutableCategoryFeatures
    }

    for (category in categories) {
        categoriesWithFeatures.add(
            CategoryOrFeatureOrEndElement(categoryId = category.categoryId, categoryName = category.name)
        )

        val featuresFromMap = featureMap[category.categoryId]

        categoriesWithFeatures.addAll(featuresFromMap!!)
        
        categoriesWithFeatures.add(
            CategoryOrFeatureOrEndElement(categoryId = category.categoryId)
        )
    }

    return categoriesWithFeatures
}

Я попытался переписать код, но в итоге сложность по времени получилась такая же. Пока что не придумал другого варианта.

Действительно, во втором цикле из-за функции addAll (которая циклически добавляет все элементы списка featuresFromMap) у нас снова получаются вложенные циклы. При этом читаемость кода снизилась.

К слову, весь первый цикл делает почти то же самое, что и функция groupBy. Разница в том, что groupBy вернёт Map<Int, List<Feature>>, а цикл — MutableMap<Int, List<CategoryOrFeatureOrEndElement>>.

Мне не нравится моя мапа на самом деле. Я добавил список не фичей, а этого класса только из-за того, что не хотел дополнительно забивать память потом. Хотя я, может быть, неправильно посчитал, и такой вариант ничем не отличается. Короче, в промышленном коде так делать не стоит.

Комментарий победителя конкурса и его решение

Давид Жубрёв, победитель в номинации «Изящный код», разрешил опубликовать его решение*:

fun getResultList() =
    getCategories().flatMap { category ->
        listOf(
            category,
            *getFeatures().filter { it.categoryId == category.categoryId }.toTypedArray(),
            category.categoryId
        )
    }

Рассмотрим, что здесь происходит. Метод flatMap в соответствии с лямбда-функцией преобразует каждый элемент списка категорий в List, где первый элемент — объект категории. Список характеристик фильтруется по ID категории и преобразовывается в массив. С помощью оператора * каждый элемент массива вкладывается в упомянутый List. Последний элемент List — ID категории.
После этого flatMap избавляется от вложенных списков и возвращает одномерный список всех элементов.

Хоть в результате и получается List<Any>, решение очень лаконичное и понятное, что идеально соответствует номинации. По временной сложности всё остаётся так же, но выглядит лучше. Если добавить сюда другую структуру данных, например, тот же Sealed Class, то будет, возможно, лучшее решение поставленной задачи.

Победитель прокомментировал конкурс и своё участие в нём:

Я не возлагал больших надежд на победу, для меня это был, скорее, небольшой фан. Я был очень удивлен, когда мне написали о победе, но было крайне приятно)

Я очень люблю работать с коллекциями, поэтому решение быстро пришло в голову, минут 20-30 ушло на всё, включая миллион запусков кода, чтобы ну точно все работало)

В целом, было довольно интересно участвовать, да и конкурс весьма необычный оказался, на мой взгляд)

— Давид Жубрёв, Санкт-Петербург.

Спасибо за прочтение данной статьи! Буду рад узнать ваше мнение о конкурсе и представленных решениях :)

Комментарии (40)


  1. Viacheslav01
    06.10.2023 15:19
    +10

    Т.е. в принципе сложность не имела значения? Все что увидел это M*N, хотя можно и за M+N.

    Как то так, не уверен, что работает)))

    fun getResultList() =
            buildList<Any> {
                val features = getFeatures().groupBy { it.categoryId }
                getCategories().forEach { category ->
                    add(category)
                    features[category.categoryId]?.let(::addAll)
                    add(category.categoryId)
                }
            }


    1. qvan
      06.10.2023 15:19

      Что, если данные нужно сначала вытащить из базы и их довольно много? Чистый sql?


      1. Viacheslav01
        06.10.2023 15:19

        Ну тут все четко задано, два списка )


  1. Balamuted
    06.10.2023 15:19
    +2

    Интересно было бы посоревноваться. А идея самоутвердиться за счет GPT очень неплоха, надо будет воспользоваться, когда совсем грустно бдует, эта безграмотная ботяра ничего путевого не напишет, если не было готового решения.


    1. shasoftX
      06.10.2023 15:19

      Сейчас в него добавят все текущие решения и победить его уже будет сложнее


  1. nikis05
    06.10.2023 15:19
    +12

    Следующий конкурс Сбера: напиши 30 строк кода на итераторах из стандартной библиотеки Rust и выиграй дворец в Геленджике :)


    1. Gorthauer87
      06.10.2023 15:19

      А бункер выдадут и бронепоезд впридачу?


  1. wataru
    06.10.2023 15:19
    +1

    Странно. Решение победиля, работает за квадрат, когда как решение друга работает за линию. Но короче, да. На сложность жюри было наплевать?


    Я не специалист по жаве, но наверняка есть похожее лакончиное решение, где сначала строится Map для GetFeatures, где по categoryId храниться список всех фич уже отфильтрованных, а потом строка, как в решении победителя, только не надо каждый раз вызвать GetFeatures() и фильтровать для каждой категории.


    1. Vest
      06.10.2023 15:19

      Моя знакомая запостила решение для JS. Мы вместе думали над кривым ТЗ, а фидбека она так никакого и не получила.


      1. wataru
        06.10.2023 15:19
        +3

        Весьма корявый какой-то конкурс получился.


        1. MIX2MAX
          06.10.2023 15:19
          +1

          это же Сбер, они физически неспособны к гениальности


    1. Viacheslav01
      06.10.2023 15:19

      Ну там вроде про красиво, а не про быстро. А решение друга не отличается ни красивостью, ни простотой восприятия.


      1. wataru
        06.10.2023 15:19
        +2

        В описании конкурса есть "Чистый, изящный, лаконичный, читаемый и понятный код, который работает без багов".
        Тормозящее на пустом месте решение — можно считать за баг. Неужели никто не нашелся, кто написал похожее лаконичное решение но без тормозов? Что-то типа того, что Viacheslav01 выше написал через groupBy.


        Edit:
        Ахаха. Только сейчас заметил, что вам ответил ссылкой на ваш же код.


        1. dididididi
          06.10.2023 15:19
          +1

          Насчёт читаемый и понятный у победителя всё плохо. Я котлина не знаю, но код автора понять могу, код победителя нет.


        1. RabraBabr
          06.10.2023 15:19
          +3

          Тормозящее на пустом месте решение

          "Ерунда - еще один сервер поднимем и будет норм, а в конечные требования допишем еще + 4 гига оперативы. Давай в продакшн".

          П.С. //sarcasm off


  1. dididididi
    06.10.2023 15:19
    -4

    Там по жаве было смешнее. Нужно было написать сервер, который принимает строку рест запросом, и проверяет правильность скобок.

    Что по мне, правильное решение проверять это на фронте и на слать эту хрень на сервер.


    1. Politura
      06.10.2023 15:19
      +7

      Ну такто сервер должен убедиться, что ему прислали валидный запрос, а не мусор, прежде чем начать его выполнять.


      1. Areso
        06.10.2023 15:19
        +8

        @dididididi
        никогда не доверяй клиенту =)


        1. VladimirFarshatov
          06.10.2023 15:19
          +1

          Даже строже: любые внешние данные - не верны. ;)


        1. dididididi
          06.10.2023 15:19

          Так мы ничего не делаем, мы клиенты возвращаем тру или фалс, а он дальше сам делает что хочет.


        1. Airtrain
          06.10.2023 15:19
          +1

          >никогда не доверяй клиенту =)

          нормальный человек обернет в try-catch и пускай маршаллер падает с исключением и 500м кодом, а не пишет колхозную считалку скобок.


          1. wataru
            06.10.2023 15:19
            +2

            Такие "нормальные человеки" — обычно причина всяких sql-инъекций и прочей радости.


      1. dididididi
        06.10.2023 15:19

        Ну так задача чтоб проверить запрос и вернуть его. Ладно бы дальше что-то делать. Но тут просто вернуть тру или false надо было


      1. dididididi
        06.10.2023 15:19

        Если в рамках чисто этой задачке, то мы ничего не делаем с запросом. Мы просто возвращаем тру или фалс. А клиент может ваще ответ заигнорить


        1. wataru
          06.10.2023 15:19

          Ну если давать задание писать полноценный сервер, который делает то да се, то там любое решение будет в колобайты кода. Это не так просто проверять, да и писать сильно дольше. В соревнованиях дают ограниченные мелкие задачки обычно.


          1. dididididi
            06.10.2023 15:19

            Ну там надо было написать тесты, документацию и СОЛИД (вкряить интерфейс чтоли?) и ещё чото. Меня это тоже удивило.

            Ну кратенько если писать на жаве это одна строчка, не считая самого метода. Но если с тестами и интерфейсами...


  1. dididididi
    06.10.2023 15:19
    +6

    Вот, кстати, как проверять читаемость кода? По идее берём 10 джунов и просим объяснить что этот код делает. Смотрим, сколько поняли и ставим оценку равную этому числу.

    Поэтому весь код со странными, редкими, непонятными функциями сразу в пролёте, а код пишется на циклах и ифчиках.

    Стримы и функциональщину многие не понимают и не любят.


    1. bromzh
      06.10.2023 15:19
      +1

      А если джунов учить стримам и функциональщине, а не императивщине, то решение победителя они поймут, а автора - нет. Вообще, может в жава-мире это не распространено, но во многих других языках работа с коллекциями через map/filter/reduce и другие подобные штуки чуть ли не по-умолчанию уже.


      1. dididididi
        06.10.2023 15:19
        -1

        В жаве тоже распространено. И тоже по умолчанию.

        Но жава - это ООП и учить функциональщину странно.

        Это вопрос про методики оценки читаемости и понятности. Мы берем десять случайных джунов и показываем им код. И если они первый понимают, а второй нет, то второй кол автоматом идёт нафиг, как нечитаемый.

        Мы не знаем какие джуны, чему их учили и как. Просто случайная выборка. Я сам не знаю результатов, мне интересно было бы узнать.


        1. bromzh
          06.10.2023 15:19
          +1

          Но жава - это ООП и учить функциональщину странно.

          Сейчас почти все языки - смесь ООП и функциональщины. И Java старается не отставать.

          Не, можно конечно писать в старом-добром стиле Java 1.5, но зачем люди вводят все эти functional interface и прочие плюшки? Тем более, им уже куча лет, на самом деле.

          В жаве тоже распространено. И тоже по умолчанию.

          Тогда какие проблемы с пониманием кода могут быть? Вы, например, писали, что код не поняли. Но это же буквально условие задачи, записанное в виде кода

          fun getResultList() =
              getCategories().flatMap { category -> // для каждой категории 
                  listOf( // составляем список, в котором
                      // Первый элемент связан с категорией (Category). Хранит в себе всю информацию о категории.
                      category,
                      // Далее идут все элементы, связанные с характеристикой (Feature) относящиеся к данной категории.
                      *getFeatures().filter { it.categoryId == category.categoryId }.toTypedArray(), 
                      // После последней характеристики, относящийся к открытой категории, идет элемент, сигнализирующий о том, что категория закончилась. Хранит в себе только CategoryId.
                      category.categoryId
                  )
              } // ну и flatmap для создание плоского массива

          Читаемо, лаконично, понятно (но долго по времени выполнения, да).

          А вот полотна кода автора сложны, потому что длинные. Тут даже не нужно особо знать сигнатуры функций, из названий flatMap и listOf всё понятно. Или это настолько редкие функции, что их никто из джунов (и видимо не только джунов) не знает? Я правда не могу понять

          Ps котлин я тоже не знаю


          1. dididididi
            06.10.2023 15:19
            -1

            Зная ответ, я тоже понимаю этот код) смысл в том, чтоб понимать его неё зная ответа))

            Но вот *, и totypedarray, что делают мне непонятно,не зная ответа)

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


  1. zubrbonasus
    06.10.2023 15:19

    Автору поста было бы полезно прочитать книгу "Чистый код".


  1. AnthonyMikh
    06.10.2023 15:19

    Я бы сказал, что изначальная задача в принципе крайне топорная. Почему нужно возвращать плоский список в таком формате и заставлять потребителя API обрабатывать отклонения от него, если можно вернуть сразу Map<Int, Pair<String, Feature>>? Это сразу позволит получить имя категории по её ID и список соответствующих фич за O(1), что выглядит более полезной вещью, чем список с линейным лукапом, а перевод в плоскую итерацию может быть сделан и на вызывающей стороне, причём без материализации списка.


  1. kochetkov-ma
    06.10.2023 15:19

    Участвовал по категории Java. Отправил значит заявку с кодом - кнопка прожалась и никакой обратной связи, никаких уведомлений на почту или телегу.
    После даты результатов из оферты захожу на сайт - ничего про результаты, в поисковиках тоже ноль. Только HR написал из Сбера мол : вы участвовали в конкурсе, не рассматриваете ли предложени - я понял, что заявка таки дошла. Но на вопрос по результатам он предложил обратиться к организаторам, хорошо что не в отделение )


  1. iamawriter
    06.10.2023 15:19

    Всегда думал, что на такого рода конкурсах задачки уровня "hard", если использовать градацию Литкода. А тут, кажется, от силы "easy". Захотелось на скорую руку прикинуть задачку на typescript. Немножко увлекся, и получилось чуть больше, чем "на скорую руку". Жалко выбрасывать, когда можно не выбрасывать, поэтому решил поделиться своим текущим видением "красоты" и выложить, вдруг какому-нибудь начинающему пригодится. Да и продвинутым тоже может пригодиться - чтобы поупражняться в критике.

    // combine.ts
    
    type CategoryId = number
    type FeatureId = number
    
    export type Category = {
        categoryId: CategoryId
        name: string
    }
    
    export type Feature = {
        featureId: FeatureId
        categoryId: CategoryId
        title: string
        value: number
    }
    
    // m+n space complexity
    // m+n time complexity
    export function combine(categories: Category[], features: Feature[]) {
        const categoryMap = new Map(categories.map((c) => [c.categoryId, c]))
    
        const featureMap: Map<CategoryId, Set<Feature>> = new Map()
    
        // "features" may contain a non-existent category in "categories",
        // so you need to check it out
        for (const feature of features) {
            if (categoryMap.has(feature.categoryId)) {
                const featureSet = featureMap.get(feature.categoryId) || new Set()
                featureSet.add(feature)
                featureMap.set(feature.categoryId, featureSet)
            } // else -> found a feature with categoryId that isn't present in "categories"
        }
    
        const combined: (Category | Feature | CategoryId)[] = []
    
        for (const [categoryId, featureSet] of featureMap.entries()) {
            combined.push(categoryMap.get(categoryId) as Category)
            combined.push(...featureSet)
            combined.push(categoryId)
        }
    
        return combined
    }

    Протестируем ф-ию combine, хотя бы и на одном примере:

    // test.ts
    
    import type { Category, Feature } from "./combine"
    import { combine } from "./combine"
    
    const C0 = { categoryId: 0, name: "category-0" }
    const C1 = { categoryId: 1, name: "category-1" }
    const C_WITHOUT_FEATURES = { categoryId: 2, name: "category-2" }
    
    const Categories: Category[] = [C0, C1, C_WITHOUT_FEATURES]
    
    const F00 = {
        featureId: 0,
        categoryId: 0,
        title: "feature-0-category-0",
        value: 0,
    }
    const F10 = {
        featureId: 1,
        categoryId: 0,
        title: "feature-1-category-0",
        value: 10,
    }
    const F21 = {
        featureId: 2,
        categoryId: 1,
        title: "feature-2-category-1",
        value: 21,
    }
    const F31 = {
        featureId: 3,
        categoryId: 1,
        title: "feature-3-category-1",
        value: 32,
    }
    const F_WITHOUT_CATEGORY = {
        featureId: 4,
        categoryId: 1000,
        title: "feature-4-category-1000",
        value: 4000,
    }
    
    const Features: Feature[] = [F00, F10, F21, F31, F_WITHOUT_CATEGORY]
    
    const Combined = [C0, F00, F10, C0.categoryId, C1, F21, F31, C1.categoryId]
    
    // "naive" implementation, but it's enough for the moment
    function arraysAreEqual(arr1: any[], arr2: any[]) {
        if (arr1.length !== arr2.length) return false
        for (let i = 0; i < arr1.length; i++) {
            if (arr1[i] != arr2[i]) return false
        }
        return true
    }
    
    function test() {
        const ok = arraysAreEqual(Combined, combine(Categories, Features))
        console.log(ok ? "Ok" : "Fail")
    }
    
    test()
    

    Вроде бы работает.


    1. iamawriter
      06.10.2023 15:19

      А вот и (само)критика подоспела:

      function arraysAreEqual(arr1: any[], arr2: any[]) {
          if (arr1.length !== arr2.length) return false
          return arr1.every((a, i) => a === arr2[i])
      }
      


    1. iamawriter
      06.10.2023 15:19

      Продолжаю сеанс саморазоблачения. Можно было вместо

      combined.push(categoryMap.get(categoryId) as Category)
      сombined.push(...featureSet)
      combined.push(categoryId)
      

      написать

      combined.push(
      	categoryMap.get(categoryId) as Category)
      	...featureSet,
      	categoryId
      )
      

      Да тут как только нельзя, и красивее и компактнее, включая использование генераторов, если немного посидеть над этим... Понятно, что во время конкурса время и нервы, и особо размусоливать некогда... Ну зато хоть осознал всю глубину и пагубность своей затяжной усталости. Этим и оправдываюсь, если что)


      1. iamawriter
        06.10.2023 15:19

        Напоследок родилось вот это:

        // m+n space complexity
        // m+n time complexity
        
        export function group(categories: Category[], features: Feature[]) {
            const groupped: Map<CategoryId, [Feature[], Category]> = new Map(
                categories.map((c) => [c.categoryId, [[], c]])
            )
        
            features.forEach((f) => {
                if (groupped.has(f.categoryId)) {
                    const [features_, _] = groupped.get(f.categoryId)
                    features_.push(f)
                }
            })
        
            return [...groupped.entries()].reduce(
                (acc, [catId, [features_, category]]) => {
                    if (features_.length > 0) {
                        acc.push(category, ...features_, catId)
                    }
                    return acc
                },
                []
            )
        }

        На чем и прекращаю конвульсии


  1. SergeyA83
    06.10.2023 15:19

    Решение в итоге красивое. Попробовал чуть оптимизировать производительность:

    fun getResultList() = getFeatures().associateBy { it.categoryId }
    .let {
    getCategories().flatMap { category ->
    listOf( category,
    it[category.categoryId],
    category.categoryId
    ) }
    }


    1. SergeyA83
      06.10.2023 15:19

      Поспешил с предыдущим комментарием, поправил, в итоге:

      fun getResultList() = 
            getFeatures()
              .groupBy { it.categoryId }
              .let { getCategories()
                       .flatMap { category ->
                                  listOf(category,
                                         *it[category.categoryId]?
                                            .toTypedArray()?:emptyArray(),
                                          category.categoryId
                                        )
                                 }
                   }