Yandex Cup 2023
Yandex Cup 2023

Всю прошлую неделю проходила квалификация на 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:

Перейдем к решению задачи.

Декомпозиция

От нас требуется вернуть тип, который по объекту ищет совпадение по строке свойств.

Например, у нас есть песня (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 этапа, где каждый делится еще на два:

  1. Получения списка свойств из строки.

    1. Разбиение Path по ‘->’.

    2. Создание диапазона значений в виде массива.

  2. Генерация нового типа из Record, используя список параметров.

    1. Прохождение по атрибутам Record вниз. 

    2. Перебор коллекции значений в Record.

Задача 1.1

Для задачи A первого этапа достаточно написать рекурсивный тип, который будет разбивать строку по шаблону.

Используем следующий алгоритм:

  1. Берем текущее значение и ищем ->.

  2. Если находим, то левую часть без разделителя добавляем в массив, а над правой повторяем Шаг 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].

Для примера, опишу итерации:

  1. На первом шаге metaData->author разобьется на metaData и author. Соответственно в массив попадет [metaData] и алгоритм повторяется для author.

  2. На втором шаге разделитель найден не будет, что следующим значением станет 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. Проверяем, что в массиве уже требуемое количество элементов.

  2. Если истина, то возвращаем допустимые значения. В противном случае добавляем объект и переходим на шаг 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 как слово Ку на Плюке.

Алгоритм следующий:

  1. Получаем Record и Path

  2. Если первое свойство является ключом в объекте, то читаем значение из 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)


  1. oxilor
    30.10.2023 07:56
    +2

    Чтобы подтянуть знания в TypeScript крайне рекомендую выделить пару дней и решить все задачки https://github.com/type-challenges/type-challenges без гугла, максимум с офф докой. Довольно много задачек там крайне интересные и нетривиальные.


    1. fafnur Автор
      30.10.2023 07:56

      Интересная вещь, стоит порешать. Спасибо за ссылку.


  1. c_i_h
    30.10.2023 07:56
    +1

    Можно ещё в TS песочнице проверить) https://www.typescriptlang.org/play


    1. fafnur Автор
      30.10.2023 07:56

      Ага, забыл про это.

      Добавляю ссылку на финальное решение — typescriptlang.org/play.


  1. Alexandroppolus
    30.10.2023 07:56
    +2

    Решил почти так же, только Range чуть по другому - с флажком. В целом да, по мотивам type-challenges, если там решить несколько хардов/экстримов, то эту задачу сразу понятно как делать.

    Основной вопрос, который может появиться после прочтения — зачем и как это применяется на практике?

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


    1. fafnur Автор
      30.10.2023 07:56

      Решил почти так же, только Range чуть по другому - с флажком.

      Интересный подход, даже не думал над таким. Все больше и больше хочу пройти type-challenges.

      Да почти никак. Это такой своеобразный дзен, для ценителей.

      Согласен, что для ценителей.

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


  1. veged
    30.10.2023 07:56
    +4

    Основной вопрос, который может появиться после прочтения — зачем и как это применяется на практике?

    Сложно ответить не находясь в контексте. Субъективно, для решения большинства задач подойдут простые типы.

    Если судить по характеру примера, то видимо где‑то в Яндекс.музыке это используется.

    Если вы знаете ответ, обязательно напишите в комментариях.

    ответ знаю ;-) задача искусственная, просто чтобы было интересно решать, а сеттинг связанный с искусством во всех его проявлениях у нас в этом сезоне проходит через все задачи — но практическая пользу можно трактовать так: «если уж в таких типах разберёшься, то в реальном проекте точно сложностей не будет»


    1. fafnur Автор
      30.10.2023 07:56
      +1

      Эх, жалко.

      Но задачка забавная, спасибо!


  1. 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, если не ошибаюсь, умеет её оптимизировать и допускает гораздо большую глубину.


    1. fafnur Автор
      30.10.2023 07:56

      Так и есть. Один раз сталкивался с подобным.

      Возьму на вооружение хвостовую рекурсию.


  1. 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, и это уже будет готовый ключ.


    1. 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>>;


  1. MihailPertsev
    30.10.2023 07:56
    +1

    type 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 строчек. Жаль, что уже нельзя проверить будет на тех тестах, что в самом Яндекс контесте используется


    1. fafnur Автор
      30.10.2023 07:56

      Немного тяжело читатается, но работает. Добавил в play.


      1. MihailPertsev
        30.10.2023 07:56

        Там можно убрать все Cast, но тогда нужно немного переписать решение, иначе TypeScript будет ругаться, я делал быстро и старался, чтобы финальное решение просто работало)


        1. fafnur Автор
          30.10.2023 07:56

          Никакого осуждения :) Вы успели сдать задачку и получить баллы, а я нет.


          1. MihailPertsev
            30.10.2023 07:56

            А Вы какие задачи еще делали?


            1. fafnur Автор
              30.10.2023 07:56

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

              Еще мне понравилась задачка про веб-сокеты, но я понял, что не успею ее сделать и даже не брался.


              1. MihailPertsev
                30.10.2023 07:56

                У меня вот не получилось вторую сделать, Wrong Answer постоянно приходил, у Вас еще осталось решение? А то прям любопытно что там не так


                1. fafnur Автор
                  30.10.2023 07:56

                  У меня решения не осталось. Там же была потрясающая, настроенная среда :). Можно попробовать восстановить решение.


                  1. Alexandroppolus
                    30.10.2023 07:56

                    А где можно условия глянуть? Я не участвовал (задачу по типизации мне коллега скинул в телеграм в позавчера утром)


                    1. MihailPertsev
                      30.10.2023 07:56
                      +2

                      @Alexandroppolus Выложил все условия в репозиторий -> https://github.com/GoldStrikeArch/yandex-cup-frontend-2023
                      Под второй задачей имеется в виду задача B


                  1. MihailPertsev
                    30.10.2023 07:56

                    Да, была, но я лично не делал там, так как неудобно, что на каждый чих шел запуск кода, ну и нет prettier)


              1. Alexandroppolus
                30.10.2023 07:56

                я сделал fractal-tree, но тоже не до конца. Я схлопнул рекурсию кроме сброса контекста, и долго не мог понять, как его запихнуть

                Глянул задачи. Кажется, без рекурсии там вообще не надо возиться с контекстом, можно сразу вычислять концы отрезка (синусами/косинусами), причем только для левой половины, а для правой делать по симметрии.


                1. 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 на код, который представлен выше.


                  1. MihailPertsev
                    30.10.2023 07:56

                    Постфактум уже же нельзя никак проверить, прошло бы решение или нет? Или все-таки можно? Может после всего соревнования или через год/два/три?

                    Все-таки те задачи 2019 года, которые были в примерах, и сейчас можно решать на yandex contest и получать обратную связь


                    1. fafnur Автор
                      30.10.2023 07:56

                      Тут может ответить только кто-то из Yandex. Поговори с @veged может он знает кого-то, кто поможет узнать :)


  1. 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


    1. fafnur Автор
      30.10.2023 07:56

      Можно давать подобную задачку на собеседовании: "Напишите парсер числа из строки, используя массивы".


      1. Alexandroppolus
        30.10.2023 07:56
        +2

        Эти самодельные парсеры довольно дохлые по ограничениям) AsNumber из поста выше долетает только до 489. Вот такой вариант - до 9999. А вот - встроенный парсер.


        1. ProgMiner
          30.10.2023 07:56

          Я вот пытался на скорую руку найти в интернете, как спарсить числовой тип из строкового, но не получилось. Пришлось выкручиваться =)

          Ну и документация тоже не помогла