Недавно прошёл конкурс красоты кода от Сбера. Участие по направлению 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)
Balamuted
06.10.2023 15:19+2Интересно было бы посоревноваться. А идея самоутвердиться за счет GPT очень неплоха, надо будет воспользоваться, когда совсем грустно бдует, эта безграмотная ботяра ничего путевого не напишет, если не было готового решения.
nikis05
06.10.2023 15:19+12Следующий конкурс Сбера: напиши 30 строк кода на итераторах из стандартной библиотеки Rust и выиграй дворец в Геленджике :)
wataru
06.10.2023 15:19+1Странно. Решение победиля, работает за квадрат, когда как решение друга работает за линию. Но короче, да. На сложность жюри было наплевать?
Я не специалист по жаве, но наверняка есть похожее лакончиное решение, где сначала строится Map для GetFeatures, где по categoryId храниться список всех фич уже отфильтрованных, а потом строка, как в решении победителя, только не надо каждый раз вызвать GetFeatures() и фильтровать для каждой категории.
Viacheslav01
06.10.2023 15:19Ну там вроде про красиво, а не про быстро. А решение друга не отличается ни красивостью, ни простотой восприятия.
wataru
06.10.2023 15:19+2В описании конкурса есть "Чистый, изящный, лаконичный, читаемый и понятный код, который работает без багов".
Тормозящее на пустом месте решение — можно считать за баг. Неужели никто не нашелся, кто написал похожее лаконичное решение но без тормозов? Что-то типа того, что Viacheslav01 выше написал через groupBy.Edit:
Ахаха. Только сейчас заметил, что вам ответил ссылкой на ваш же код.dididididi
06.10.2023 15:19+1Насчёт читаемый и понятный у победителя всё плохо. Я котлина не знаю, но код автора понять могу, код победителя нет.
RabraBabr
06.10.2023 15:19+3Тормозящее на пустом месте решение
"Ерунда - еще один сервер поднимем и будет норм, а в конечные требования допишем еще + 4 гига оперативы. Давай в продакшн".
П.С. //sarcasm off
dididididi
06.10.2023 15:19-4Там по жаве было смешнее. Нужно было написать сервер, который принимает строку рест запросом, и проверяет правильность скобок.
Что по мне, правильное решение проверять это на фронте и на слать эту хрень на сервер.
Politura
06.10.2023 15:19+7Ну такто сервер должен убедиться, что ему прислали валидный запрос, а не мусор, прежде чем начать его выполнять.
Areso
06.10.2023 15:19+8@dididididi
никогда не доверяй клиенту =)dididididi
06.10.2023 15:19Так мы ничего не делаем, мы клиенты возвращаем тру или фалс, а он дальше сам делает что хочет.
dididididi
06.10.2023 15:19Ну так задача чтоб проверить запрос и вернуть его. Ладно бы дальше что-то делать. Но тут просто вернуть тру или false надо было
dididididi
06.10.2023 15:19Если в рамках чисто этой задачке, то мы ничего не делаем с запросом. Мы просто возвращаем тру или фалс. А клиент может ваще ответ заигнорить
wataru
06.10.2023 15:19Ну если давать задание писать полноценный сервер, который делает то да се, то там любое решение будет в колобайты кода. Это не так просто проверять, да и писать сильно дольше. В соревнованиях дают ограниченные мелкие задачки обычно.
dididididi
06.10.2023 15:19Ну там надо было написать тесты, документацию и СОЛИД (вкряить интерфейс чтоли?) и ещё чото. Меня это тоже удивило.
Ну кратенько если писать на жаве это одна строчка, не считая самого метода. Но если с тестами и интерфейсами...
dididididi
06.10.2023 15:19+6Вот, кстати, как проверять читаемость кода? По идее берём 10 джунов и просим объяснить что этот код делает. Смотрим, сколько поняли и ставим оценку равную этому числу.
Поэтому весь код со странными, редкими, непонятными функциями сразу в пролёте, а код пишется на циклах и ифчиках.
Стримы и функциональщину многие не понимают и не любят.
bromzh
06.10.2023 15:19+1А если джунов учить стримам и функциональщине, а не императивщине, то решение победителя они поймут, а автора - нет. Вообще, может в жава-мире это не распространено, но во многих других языках работа с коллекциями через map/filter/reduce и другие подобные штуки чуть ли не по-умолчанию уже.
dididididi
06.10.2023 15:19-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 котлин я тоже не знаю
dididididi
06.10.2023 15:19-1Зная ответ, я тоже понимаю этот код) смысл в том, чтоб понимать его неё зная ответа))
Но вот *, и totypedarray, что делают мне непонятно,не зная ответа)
Вообще самый читаемый код, это где сверху коммент есть с объяснением, что он делает, будем честны.
AnthonyMikh
06.10.2023 15:19Я бы сказал, что изначальная задача в принципе крайне топорная. Почему нужно возвращать плоский список в таком формате и заставлять потребителя API обрабатывать отклонения от него, если можно вернуть сразу
Map<Int, Pair<String, Feature>>
? Это сразу позволит получить имя категории по её ID и список соответствующих фич за O(1), что выглядит более полезной вещью, чем список с линейным лукапом, а перевод в плоскую итерацию может быть сделан и на вызывающей стороне, причём без материализации списка.
kochetkov-ma
06.10.2023 15:19Участвовал по категории Java. Отправил значит заявку с кодом - кнопка прожалась и никакой обратной связи, никаких уведомлений на почту или телегу.
После даты результатов из оферты захожу на сайт - ничего про результаты, в поисковиках тоже ноль. Только HR написал из Сбера мол : вы участвовали в конкурсе, не рассматриваете ли предложени - я понял, что заявка таки дошла. Но на вопрос по результатам он предложил обратиться к организаторам, хорошо что не в отделение )
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()
Вроде бы работает.
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]) }
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 )
Да тут как только нельзя, и красивее и компактнее, включая использование генераторов, если немного посидеть над этим... Понятно, что во время конкурса время и нервы, и особо размусоливать некогда... Ну зато хоть осознал всю глубину и пагубность своей затяжной усталости. Этим и оправдываюсь, если что)
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 }, [] ) }
На чем и прекращаю конвульсии
SergeyA83
06.10.2023 15:19Решение в итоге красивое. Попробовал чуть оптимизировать производительность:
fun getResultList() = getFeatures().associateBy { it.categoryId }
.let {
getCategories().flatMap { category ->
listOf( category,
it[category.categoryId],
category.categoryId
) }
}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 ) } }
Viacheslav01
Т.е. в принципе сложность не имела значения? Все что увидел это M*N, хотя можно и за M+N.
Как то так, не уверен, что работает)))
qvan
Что, если данные нужно сначала вытащить из базы и их довольно много? Чистый sql?
Viacheslav01
Ну тут все четко задано, два списка )