Всю прошлую неделю проходила квалификация на Yandex Cup 2023. Я решил тряхнуть стариной и вспомнить что такое спортивное программирование.
Яндекс представил 8 задачек разной сложности, которые необходимо сделать за пять часов. Я принял участие. На старте был уверен в себе. Однако, получил плохие результаты. Следующие пол дня я чувствовал уныние и разочарование. Потом пришла идея, как это использовать.
Мое внимание зацепила одна задача, связанная с типизацией в Typescript. Я давно хотел написать статью про типы, показать бекенду какие сложные интерфейсы можно создавать в Typescript.
Завариваем чаек, я начинаю.
Постановка задачи
Чтобы работать более продуктивно, многие слушают музыку. Кому‑то помогает сосредоточиться lo‑fi, кому‑то — белый шум, а кто‑то максимально эффективен, когда слушает тяжёлый металл. Учёные Института Б выяснили, что способность концентрации человека зависит от уровня серотонина в крови во время прослушивания музыки. Причём каждый жанр, и даже трек, влияют на выработку гормона по‑разному.
Ваша задача — написать программу, которая позволит получать и анализировать информацию об уровне серотонина в крови при прослушивании конкретных треков у разных фокус‑групп. Так мы сможем выявить композиции, которые лучше всего помогают настроиться на рабочий лад и повышают продуктивность.
Условие
Разработайте тип Get, который позволит по запросу выдавать параметры песни, такие как автор, дата выпуска песни или другие различные параметры и настройки музыкальных дорожек.
На входе программа должна получать константный тип массива и строку с путём в формате точечно‑скобочной нотации (например, «keys→(0–2)→val»), а возвращать — литерал значений объектов по заданному пути.
Тип должен позволить запросить информацию по определенным ключам аналогично примерам.
Пример структуры:
const song = {
metaData: {
title: 'Highway to Hell',
author: 'AC/DC',
date: '27.07.1979',
},
tracks: [
{
id: 1,
name: 'Piano',
soundType: 'virtual_instrument',
instrument: 'piano',
regions: [
{
id: 1,
start: 0,
end: 3,
midiData: [
{note: 'F4', velocity: 80, startTime: 0, duration: 1},
{note: 'D4', velocity: 80, startTime: 1, duration: 1},
{note: 'E4', velocity: 90, startTime: 2, duration: 1},
],
effects: [
{type: 'reverb', intensity: 15},
{type: 'delay', time: 0.5, feedback: 30, mix: 20},
],
},
],
pan: 5,
volume: 78,
},
{
id: 2,
name: 'Guitar',
soundType: 'virtual_instrument',
instrument: 'guitar',
regions: [
{
id: 1,
start: 0,
end: 5,
midiData: [
{note: 'C4', velocity: 10, startTime: 0, duration: 1},
{note: 'E4', velocity: 20, startTime: 1, duration: 1},
{note: 'E4', velocity: 30, startTime: 2, duration: 1},
{note: 'F4', velocity: 40, startTime: 3, duration: 1},
{note: 'D4', velocity: 50, startTime: 4, duration: 1},
],
},
],
pan: 10,
volume: 60,
},
],
} as const;
Примеры тестов:
type songAuthor = Get<typeof song, 'metaData->author'>; // AC/DC
type firstTrackVolume = Get<typeof song, 'tracks->0->volume'>; // 78
type tracksVolume = Get<typeof song, 'tracks->(0-2)->volume'>; // 78 | 60
type notes = Get<typeof song, 'tracks->1->regions->0->midiData->(0-5)->note'>; // "F4" | "D4" | "E4" | "C4"
type midiData = Get<typeof song, 'tracks->1->regions->0->midiData->(0-2)'>; // { note: "C4", velocity: 10,
startTime: 0, duration: 1, } | { note: "E4", velocity: 20, startTime: 1, duration: 1 }
type thirdNoteVelocity = Get<typeof song, 'tracks->1->regions->0->midiData->3->velocity'>; // 40
Ваше решение должно содержать определение типа Get
.
type Get<T extends unknown, Path extends string> = ...
Основной вопрос, который может появиться после прочтения — зачем и как это применяется на практике?
Сложно ответить не находясь в контексте. Субъективно, для решения большинства задач подойдут простые типы.
Если судить по характеру примера, то видимо где‑то в Яндекс.музыке это используется.
Если вы знаете ответ, обязательно напишите в комментариях.
Необходимый минимум
Если вы не знакомы с Typescript, то советую обратиться к документации.
Описать принцип работы типов в Typescript можно с помощью утверждения:
Если что-то выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, и есть утка.
Необходимые познания в Typescript:
Conditional Types, особенно infer
Перейдем к решению задачи.
Декомпозиция
От нас требуется вернуть тип, который по объекту ищет совпадение по строке свойств.
Например, у нас есть песня (song
).
const song = {
metaData: {
author: 'AC/DC'
}
};
И мы хотим узнать ее автора (author
).
Тогда цепочка обращений будет следующей: song.metaData.author
. В строковом представлении — metaData->author
.
По всей видимости, Yandex любит PHP, и в качестве разделителя использует → вместо
Нам необходимо написать type Get, который по объекту, вернет новый тип. В данном случае — AC/DC
.
type Get<Record, Path> = <some code>;
type author = Get<typeof song, 'metaData->author'>; // AC/DC
Задачу разобьем на 2 этапа, где каждый делится еще на два:
-
Получения списка свойств из строки.
Разбиение Path по ‘->’.
Создание диапазона значений в виде массива.
-
Генерация нового типа из Record, используя список параметров.
Прохождение по атрибутам Record вниз.
Перебор коллекции значений в Record.
Задача 1.1
Для задачи A первого этапа достаточно написать рекурсивный тип, который будет разбивать строку по шаблону.
Используем следующий алгоритм:
Берем текущее значение и ищем
->
.Если находим, то левую часть без разделителя добавляем в массив, а над правой повторяем Шаг 1.
type Split<
Value extends string,
Pattern extends string = '->'
> = Value extends `${infer LValue}${Pattern}${infer RValue}`
? [LValue, ...Split<RValue, Pattern>]
: [Value];
Выражение — Value extends ${infer LValue}${Pattern}${infer RValue}
проверяет, можно ли из входного параметра Value
выделить 3 объекта: LValue
, Pattern
и RValue
.
Infer
позволяет создать условный тип. В данном случае, истинным выражением будет если найдено ->
, где звездочка это любое значение.
Корректные варианты: a->b
, a->
, ->b
, ->
.
Отмечу,
Split
не проверяет невалидные кейсы. Полагается, что каждый разделитель с обеих сторон имеет соседей, который не является → или пустой строкой. То есть, решения сmeta->''
или->->
считаются неверными.
Для определения рекурсивного типа, достаточно вызвать самого себя, но уже с измененными параметрами: [LValue, ...Split<RValue, Pattern>]
В коллекцию добавляется LValue
, а затем вызывается Split
с RValue
. Если не выполняется проверка, то возвращается сам объект в виде массива — [Value
].
Для примера, опишу итерации:
На первом шаге
metaData->author
разобьется наmetaData
иauthor
. Соответственно в массив попадет [metaData
] и алгоритм повторяется дляauthor
.На втором шаге разделитель найден не будет, что следующим значением станет
author
.
В итоге получается type = [metaData,author]
.
Задача 1.2
Задача сводится к созданию массива с фиксированным диапазоном.
Для выражений (0-2)
и (3-6)
должны вернуться результаты [0, 1]
и [3, 4, 5]
.
Используя подход из Split
, аналогично найдем Start
и End
.
type Range<T extends string> = T extends `(${infer Start}-${infer End})`
? [Start, End]
: never;
Стоит отметить, что в данном случае мы получим коллекцию строк, а для создания диапазона необходим number
.
Если при приведении типов в JavaScript можно просто добавить плюс перед значением (
+stringValue
) или обернуть вNumber(stringValue)
, то с типизацией это не пройдет.
Для преобразования из строки в число также infer
:
type StringToInt<Value extends string> = Value extends `${infer Digit extends number}`
? Digit
: never;
Очень интересна сама реализация в Typescript. Я сталкивался с написанием трансляторов, но разработка оператора infer это что‑то из параллельной вселенной.
Упростим решение и сделаем проверку внутри:
type Range<T extends string> = T extends `(${infer Start extends number}-${infer End extends number})`
? [Start, End]
: never;
Создадим перечисление фиксированной длины.
type Enumerate<
Length extends number,
Accumulation extends number[] = []
> = Accumulation['length'] extends Length
? Accumulation[number]
: Enumerate<Length, [...Accumulation, Accumulation['length']]>;
Использование рекурсии просто зашкаливает. Стоит переименовать Typescript в RecursiveScript.
Принцип работы алгоритма следующий:
Проверяем, что в массиве уже требуемое количество элементов.
Если истина, то возвращаем допустимые значения. В противном случае добавляем объект и переходим на шаг 1.
Пример использования Enumerate:
type Arr = Enumerate<4>; // 0 | 1 | 2 | 3
Самое неочевидное в реализации — это Accumulation[number]
.
Тут number
выступает не в качестве численного типа, а в виде указания обращения к перечисляемому свойству.
Так как
Accumulation
это массив, тоAccumulation[number]
формально говорит компилятору — верни все возможные значения.
Достаточно убрать number и тогда Enumerate будет возвращать массив [0, 1, 2, …]
.
type Enumerate<
Length extends number,
Accumulation extends number[] = []
> = Accumulation['length'] extends Length
? Accumulation
: Enumerate<Length, [...Accumulation, Accumulation['length']]>;
Теперь type Arr = Enumerate<2>
вернет [0, 1]
.
Но как тогда получить диапазон значений?
Нужно создать два списка (Start
, End
) и вычесть из End - Start
.
Реализации TS будет выглядеть следующим образом:
type RangeArray<
Start extends number,
End extends number
> = Exclude<Enumerate<End>, Enumerate<Start>>;
Вычитание сделано с помощью стандартной утилиты — Exclude.
Финальная версия примет вид:
type RangeArray<
Range extends string
> = Range extends `(${infer Start extends number}-${infer End extends number})`
? Exclude<Enumerate<End>, Enumerate<Start>>
: never;
После того как мы научились разбивать строку на коллекцию параметров, а также превращать диапазон в массив значений, можно перейти к следующему этапу.
Задача 2.1
Необходимо создать тип, который будет выбирать из объекта данные по предоставленному списку свойств.
Можно выделить два кейса:
в навигации нету перебора коллекций
metaData->author
.необходимо пройти несколько элементов
tracks->(0-2)->volume
.
Начнем решение с реализации первого варианта.
Снова используем infer
.
Infer
имеет такое же значение в Typescript как словоКу
на Плюке.
Алгоритм следующий:
Получаем
Record
иPath
.Если первое свойство является ключом в объекте, то читаем значение из
Record
и переходим на шаг 1. Иначе возвращаем самRecord
.
Реализовать это можно следующим образом:
type ReadProperty<
Record,
Path
> = Path extends [first: infer FirstProperty extends keyof Record, ...args: infer OtherProperties]
? ReadProperty<Record[FirstProperty], OtherProperties>
: Record;
Проверяем утверждение — Path extends [first: infer FirstProperty extends keyof Record,...args: infer OtherProperties]
.
Если оно валидно, то тогда, рекурсивно вызываем ReadProperty<Record[FirstProperty], OtherProperties>
.
В противном случае, ничего делать не требуется и следует передать обратно Record
.
Как так выходит, что возвращается неизмененный Record?
Когда мы спустились до низа Record
, то у нас будет объект и нет аргумента.
Пример. Наша цель найти автора.
type Record = {
metaData: {
title: 'Highway to Hell',
author: 'AC/DC',
date: '27.07.1979',
},
};
type Path = ['metaData', 'author'];
На первой итерации условие выполняется и мы идем дальше.
type Record = {
title: 'Highway to Hell',
author: 'AC/DC',
date: '27.07.1979',
};
type Path = ['author'];
Спускаемся еще ниже.
type Record = 'AC/DC';
type Path = [];
Как видно из примера, мы в самом низу и поэтому просто возвращаем Record
.
Немного изменим реализацию и учтем, что свойство из Path
может быть не только ключом, но и диапазоном.
type ReadProperty<
Record,
Path
> = Path extends [first: infer FirstProperty extends string, ...args: infer OtherProperties]
? FirstProperty extends keyof Record
? ReadProperty<Record[FirstProperty], OtherProperties>
: never // Array
: Record;
Теперь FirstProperty
является просто строкой, а не частью Record
. Из-за этого добавилась проверка на принадлежность FirstProperty
части Record
.
В месте, где возвращается never, необхоидмо добавить логику с просмотром диапазона значений. Это будет сделано в следующей задаче.
Задача 2.2
Задача сводится к тому, чтобы пройтись по диапазону и выбрать нужные значения.
Вот на этом моменте я и застопорился на Cup-е. Решение оказалось простым до гениальности:
Для того чтобы выполнить обход, достаточно использовать объединение - “|”.
Пример реализации:
type ReadArrayProperty<
Array,
Record,
Path
> = Array extends [first: infer FirstElem extends keyof Record, ...args: infer OtherElements]
? ReadProperty<Record[FirstElem], Path> | ReadArrayProperty<OtherElements, Record, Path>
: never;
В данном случае мы перебираем массив с помощью infer.
Сначала мы вызываем ReadProperty
, а уже потом снова рекурсивно ReadArrayProperty
.
На понимание того, как применить тип для первого элемента и затем пройти по оставшимся, я потратил больше двух часов.
На то оно и спортивное программирование - чьи мозги соображают быстрее, тот и побеждает.
Полное решение
Если объединить все вышесказанное, то итоговое решение получается следующим:
type Split<
Value extends string,
Pattern extends string = '->'
> = Value extends `${infer LValue}${Pattern}${infer RValue}`
? [LValue, ...Split<RValue, Pattern>]
: [Value];
type Enumerate<
Length extends number,
Accumulation extends number[] = []
> = Accumulation['length'] extends Length
? Accumulation
: Enumerate<Length, [...Accumulation, Accumulation['length']]>;
type ReadArrayProperty<
Array,
Record,
Path
> = Array extends [first: infer FirstElem extends keyof Record, ...args: infer OtherElements]
? ReadProperty<Record[FirstElem], Path> | ReadArrayProperty<OtherElements, Record, Path>
: never;
type ReadProperty<
Record,
Path
> = Path extends [first: infer FirstProperty extends string, ...args: infer OtherProperties]
? FirstProperty extends keyof Record
? ReadProperty<Record[FirstProperty], OtherProperties>
: ReadArrayProperty<RangeArray<FirstProperty>, Record, OtherProperties>
: Record;
type RangeArray<
Range extends string
> = Range extends `(${infer Start extends number}-${infer End extends number})`
? Exclude<Enumerate<End>, Enumerate<Start>>
: never;
type Get<T, Path extends string> = ReadProperty<T, Split<Path>>;
Примеры для проверки:
type songAuthor = Get<typeof song, 'metaData->author'>; // AC/DC
type firstTrackVolume = Get<typeof song, 'tracks->0->volume'>; // 78
type tracksVolume = Get<typeof song, 'tracks->(0-2)->volume'>; // 78 | 60
type notes = Get<typeof song, 'tracks->1->regions->0->midiData->(0-5)->note'>; // "F4" | "D4" | "E4" | "C4"
type midiData = Get<typeof song, 'tracks->1->regions->0->midiData->(0-2)'>; // { note: "C4", velocity: 10, startTime: 0, duration: 1, } | { note: "E4", velocity: 20, startTime: 1, duration: 1 }
type thirdNoteVelocity = Get<typeof song, 'tracks->1->regions->0->midiData->3->velocity'>; // 40
Я не проверял эту финальную версию в Яндекс.Контесте, так как доделал задачу после завершения отведенного времени.
Будет забавно, если оно неверно.
Я доверился только WebStorm:
Напишите в комментариях свою реализацию. Мне очень интересно, можно ли как-то упростить. Есть шанс, что лучшее решение будет на две строчки.
UPD: В комментариях есть обновленная версия, в которой убрана задача 2.2. Вместо обхода массива, можно напрямую использовать перечисление. Спасибо @Alexandroppolus за подсказку.
Комментарии (31)
c_i_h
30.10.2023 07:56+1Можно ещё в TS песочнице проверить) https://www.typescriptlang.org/play
fafnur Автор
30.10.2023 07:56Ага, забыл про это.
Добавляю ссылку на финальное решение — typescriptlang.org/play.
Alexandroppolus
30.10.2023 07:56+2Решил почти так же, только Range чуть по другому - с флажком. В целом да, по мотивам type-challenges, если там решить несколько хардов/экстримов, то эту задачу сразу понятно как делать.
Основной вопрос, который может появиться после прочтения — зачем и как это применяется на практике?
Да почти никак. Это такой своеобразный дзен, для ценителей. Подобную задачу не зададут даже на собесе, не говоря уж о рабочем коде.
fafnur Автор
30.10.2023 07:56Решил почти так же, только Range чуть по другому - с флажком.
Интересный подход, даже не думал над таким. Все больше и больше хочу пройти type-challenges.
Да почти никак. Это такой своеобразный дзен, для ценителей.
Согласен, что для ценителей.
Просто иногда рассказываешь про возможности языка, что так можно, а еще и вот так. А тебе говорят, что на практике не применимо, бесполезная вещь. Охота иметь аргумент.
veged
30.10.2023 07:56+4Основной вопрос, который может появиться после прочтения — зачем и как это применяется на практике?
Сложно ответить не находясь в контексте. Субъективно, для решения большинства задач подойдут простые типы.
Если судить по характеру примера, то видимо где‑то в Яндекс.музыке это используется.
Если вы знаете ответ, обязательно напишите в комментариях.
ответ знаю ;-) задача искусственная, просто чтобы было интересно решать, а сеттинг связанный с искусством во всех его проявлениях у нас в этом сезоне проходит через все задачи — но практическая пользу можно трактовать так: «если уж в таких типах разберёшься, то в реальном проекте точно сложностей не будет»
Alexandroppolus
30.10.2023 07:56+2Кстати, тип Split из решения автора упирается в довольно суровые ограничения на глубину рекурсии - 50 итераций. Можно сильно увеличить, если переписать на хвостовую рекурсию:
type Split< Value extends string, Pattern extends string = '->', A extends string[] = [], > = Value extends ${infer LValue}${Pattern}${infer RValue} ? Split<RValue, Pattern, [...A, LValue]> : [...A, Value];
Любую рекурсию, как известно, можно переделать на хвостовую. TS начиная с версии 4.5, если не ошибаюсь, умеет её оптимизировать и допускает гораздо большую глубину.
fafnur Автор
30.10.2023 07:56Так и есть. Один раз сталкивался с подобным.
Возьму на вооружение хвостовую рекурсию.
Alexandroppolus
30.10.2023 07:56+2И ещё поинт: ReadArrayProperty не нужен. Не надо вручную обходить набор ключей, TS сделает это сам, если ключи заданы объединением. Вот, с набором 0 | 1:
type TSong = typeof song; type TrackNames = TSong['tracks'][0 | 1]['name']; // "Piano" | "Guitar"
Так что достаточно превратить, например, '(1-3)' в объединение 1 | 2 | 3, и это уже будет готовый ключ.
fafnur Автор
30.10.2023 07:56Да, я увидел это еще в вашем решении.
Если переписать без обхода массива, то финальное решение немного укоротиться — play.
type Split< Value extends string, Pattern extends string = '->', Accumulation extends string[] = [] > = Value extends `${infer LValue}${Pattern}${infer RValue}` ? Split<RValue, Pattern, [...Accumulation, LValue extends `(${infer Start extends number}-${infer End extends number})`? Exclude<Enumerate<End>, Enumerate<Start>> : LValue]> : [...Accumulation, Value]; type Enumerate< Length extends number, Accumulation extends number[] = [] > = Accumulation['length'] extends Length ? Accumulation[number] : Enumerate<Length, [...Accumulation, Accumulation['length']]>; type ReadProperty< Record, Path > = Path extends [first: infer FirstProperty extends keyof Record, ...args: infer OtherProperties] ? ReadProperty<Record[FirstProperty], OtherProperties> : Record; type Get<T, Path extends string> = ReadProperty<T, Split<Path>>;
MihailPertsev
30.10.2023 07:56+1type UnionOfNumbersLessThan<N extends number, HelperArray extends number[] = []> = N extends HelperArray['length'] ? HelperArray[number] : UnionOfNumbersLessThan<N, [...HelperArray, HelperArray['length']]> type NumberRange<L extends number, H extends number> = H | Exclude<UnionOfNumbersLessThan<H>, UnionOfNumbersLessThan<L>> type ToNumber<S extends string> = S extends `${infer N extends number}` ? N : never; type Cast<A1 extends any, A2 extends any> = A1 extends A2 ? A1 : A2 // split k1->k2->(s-e)->k3 like structure to array of keys type SplitPath<Path extends string> = Path extends `${infer First}->${infer Rest}`? [First, ... SplitPath<Rest>] :[Path] type RecursivePick<T extends unknown, Keys extends string[]> = Keys extends [infer Key, ...infer Rest] ? Key extends `(${infer Start}-${infer End})` // If we have (start-end) range then //@ts-ignore ? RecursivePick<T[Cast<NumberRange<ToNumber<Start>, ToNumber<End>>, keyof T> extends T[Cast<Key, keyof T>] ? T[Cast<Key, keyof T>] : never], Cast<Rest, string[]>> // else recursively get property : RecursivePick<T[Cast<Key, keyof T>], Cast<Rest, string[]>> : T type Get<T extends unknown, Path extends string> = SplitPath<Path>[0] extends keyof T // if First key exists in object ? SplitPath<Path> extends [infer Key, ...infer Rest] ? RecursivePick<Cast<T[Cast<Key, keyof T>], object>, Cast<Rest, string[]>> : never : never // TESTS! type songAuthor = Get<typeof song, 'metaData->author'>; // AC/DC type firstTrackVolume = Get<typeof song, 'tracks->0->volume'>; // 78 type tracksVolume = Get<typeof song, 'tracks->(0-2)->volume'>; // 78 | 60 type tracksVolume2 = Get<typeof song, 'tracks->(0-2)->regions->(0-2)->end'>; // 78 | 60 type notes = Get<typeof song, 'tracks->1->regions->0->midiData->(0-5)->note'>; // "F4" | "D4" | "E4" | "C4" type midiData = Get<typeof song, 'tracks->1->regions->0->midiData->(0-2)'>; // { note: "C4", velocity: 10, // startTime: 0, duration: 1, } | { note: "E4", velocity: 20, startTime: 1, duration: 1 } type thirdNoteVelocity = Get<typeof song, 'tracks->1->regions->0->midiData->3->velocity'>; // 40 type qwe = Get<typeof song, 'lala->nana'>; // never type asd = Get<typeof song, 'metaData->nana'>; // "Highway to Hell" | "AC/DC" | "27.07.1979"
Вот мое решение, которое набрало 40/40 баллов, по сути суть та же, думаю можно сократить до 20 строчек. Жаль, что уже нельзя проверить будет на тех тестах, что в самом Яндекс контесте используется
fafnur Автор
30.10.2023 07:56Немного тяжело читатается, но работает. Добавил в play.
MihailPertsev
30.10.2023 07:56Там можно убрать все Cast, но тогда нужно немного переписать решение, иначе TypeScript будет ругаться, я делал быстро и старался, чтобы финальное решение просто работало)
fafnur Автор
30.10.2023 07:56Никакого осуждения :) Вы успели сдать задачку и получить баллы, а я нет.
MihailPertsev
30.10.2023 07:56А Вы какие задачи еще делали?
fafnur Автор
30.10.2023 07:56Не считая первых двух, я сделал fractal-tree, но тоже не до конца. Я схлопнул рекурсию кроме сброса контекста, и долго не мог понять, как его запихнуть. В итоге решение пришло уже после завершения.
Еще мне понравилась задачка про веб-сокеты, но я понял, что не успею ее сделать и даже не брался.MihailPertsev
30.10.2023 07:56У меня вот не получилось вторую сделать, Wrong Answer постоянно приходил, у Вас еще осталось решение? А то прям любопытно что там не так
fafnur Автор
30.10.2023 07:56У меня решения не осталось. Там же была потрясающая, настроенная среда :). Можно попробовать восстановить решение.
Alexandroppolus
30.10.2023 07:56А где можно условия глянуть? Я не участвовал (задачу по типизации мне коллега скинул в телеграм в позавчера утром)
MihailPertsev
30.10.2023 07:56+2@Alexandroppolus Выложил все условия в репозиторий -> https://github.com/GoldStrikeArch/yandex-cup-frontend-2023
Под второй задачей имеется в виду задача B
MihailPertsev
30.10.2023 07:56Да, была, но я лично не делал там, так как неудобно, что на каждый чих шел запуск кода, ну и нет prettier)
Alexandroppolus
30.10.2023 07:56я сделал fractal-tree, но тоже не до конца. Я схлопнул рекурсию кроме сброса контекста, и долго не мог понять, как его запихнуть
Глянул задачи. Кажется, без рекурсии там вообще не надо возиться с контекстом, можно сразу вычислять концы отрезка (синусами/косинусами), причем только для левой половины, а для правой делать по симметрии.
fafnur Автор
30.10.2023 07:56Если я догадался бы загуглить, что такое фрактальные деревья, то возможно пошел бы другим путем.
// drawTree.js const getColor = function () { const colors = {}; return function (level) { if(typeof colors[level] !== "undefined") { return colors[level]; } const color = computeColor(level); colors[level] = color; return color; } }(); const getWidth = function createComputeWidth() { const width = {}; return function (level) { if(typeof width[level] !== "undefined") { return width[level]; } const color = computeWidth(level); width[level] = color; return color; } }(); function draw(startY, angle, level) { const startX = canvas.width / 2; const len = length * Math.pow(depth, level); ctx.beginPath(); ctx.save(); ctx.translate(level ? 0 : startX, startY); ctx.rotate(angle * Math.PI / 180); ctx.moveTo(0, 0); ctx.lineTo(0, -len); ctx.strokeStyle = getColor(level); ctx.lineWidth = getWidth(level); ctx.stroke(); if (len < 10) { ctx.restore(); } } function drawTree(startY, angle, level = 0) { const restore = '__restore__'; const stack = []; stack.push([restore]); stack.push([startY, angle, level]); do { const [y, an, lev] = stack.pop(); if(y === restore) { ctx.restore(); continue; } const len = length * Math.pow(depth, lev); draw(y, an, lev); if (len >= 10) { stack.push([restore]); stack.push([-len, an - angleOffset, lev + 1]); stack.push([-len, an + angleOffset, lev + 1]); } } while (stack.length > 0); }
Думаю, такое решение прошло бы.
Исходники для задачи можно взять тут github, спасибо за это @MihailPertsev.
Там только заменить
drawTree.js
на код, который представлен выше.MihailPertsev
30.10.2023 07:56Постфактум уже же нельзя никак проверить, прошло бы решение или нет? Или все-таки можно? Может после всего соревнования или через год/два/три?
Все-таки те задачи 2019 года, которые были в примерах, и сейчас можно решать на yandex contest и получать обратную связь
ProgMiner
30.10.2023 07:56Ух ты, не знал что можно с помощью infer выводить числа из строки! Я для этого задания написал свой парсер чисел используя массивы в качестве чисел...
Текст решения
type Add<A extends any[], B extends any[]> = [...A, ...B]; type Mul<A extends any[], B extends any[]> = A extends [infer H, ...infer T] ? [...B, ...Mul<T, B>] : []; type Ten = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; type AsNumber<N extends string, Acc extends any[] = []> = N extends `0${infer R}` ? AsNumber<R, Mul<Acc, Ten>> : N extends `1${infer R}` ? AsNumber<R, Add<Mul<Acc, Ten>, [1]>> : N extends `2${infer R}` ? AsNumber<R, Add<Mul<Acc, Ten>, [1, 2]>> : N extends `3${infer R}` ? AsNumber<R, Add<Mul<Acc, Ten>, [1, 2, 3]>> : N extends `4${infer R}` ? AsNumber<R, Add<Mul<Acc, Ten>, [1, 2, 3, 4]>> : N extends `5${infer R}` ? AsNumber<R, Add<Mul<Acc, Ten>, [1, 2, 3, 4, 5]>> : N extends `6${infer R}` ? AsNumber<R, Add<Mul<Acc, Ten>, [1, 2, 3, 4, 5, 6]>> : N extends `7${infer R}` ? AsNumber<R, Add<Mul<Acc, Ten>, [1, 2, 3, 4, 5, 6, 7]>> : N extends `8${infer R}` ? AsNumber<R, Add<Mul<Acc, Ten>, [1, 2, 3, 4, 5, 6, 7, 8]>> : N extends `9${infer R}` ? AsNumber<R, Add<Mul<Acc, Ten>, [1, 2, 3, 4, 5, 6, 7, 8, 9]>> : Acc; type Get<T extends unknown, Path extends string> = Path extends '' ? T : Path extends `${infer Key}->${infer Rest}` ? Get1<T, Key, Rest> : Get1<T, Path, ''>; type Get1<T extends unknown, Key extends string, Rest extends string> = Key extends `(${infer L}-${infer R})` ? GetLR<T, AsNumber<L>, AsNumber<R>, Rest> : T extends Record<Key, unknown> ? Get<T[Key], Rest> : never; type GetLR<T extends unknown, L extends any[], R extends any[], Rest extends string> = T extends Record<L["length"], unknown> ? ( R extends L ? Get<T[L["length"]], Rest> : Get<T[L["length"]], Rest> | GetLR<T, [1, ...L], R, Rest> ) : never
fafnur Автор
30.10.2023 07:56Можно давать подобную задачку на собеседовании: "Напишите парсер числа из строки, используя массивы".
Alexandroppolus
30.10.2023 07:56+2Эти самодельные парсеры довольно дохлые по ограничениям) AsNumber из поста выше долетает только до 489. Вот такой вариант - до 9999. А вот - встроенный парсер.
ProgMiner
30.10.2023 07:56Я вот пытался на скорую руку найти в интернете, как спарсить числовой тип из строкового, но не получилось. Пришлось выкручиваться =)
Ну и документация тоже не помогла
oxilor
Чтобы подтянуть знания в TypeScript крайне рекомендую выделить пару дней и решить все задачки https://github.com/type-challenges/type-challenges без гугла, максимум с офф докой. Довольно много задачек там крайне интересные и нетривиальные.
fafnur Автор
Интересная вещь, стоит порешать. Спасибо за ссылку.