У меня есть интересная проблема, я не знаю, как ее решить. Я бегло знаком с функциями возврата, но я озадачен следующей загадкой:

Предположим, у нас есть коллекция объектов, каждый из которых определяет массив частей url, например ['/root', '/nested', '/nested', '/leaf']. Учитывая массив частей URL, найдите объект, чьи части URL точно совпадают. Это довольно просто, но вот кикер: части URL, определяемые объектами, также могут быть символами подстановки, например, ['/root', '/:id', '/nested', ':name', '/leaf']. Теперь это становится интересным, потому что точное совпадение важнее, чем сопоставление с подстановочным знаком, и точные совпадения должны соблюдаться как можно дольше, даже если оно заканчивается большинством подстановочных знаков.

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

Вот что я ищу:

var objects =  [
  { id: 1, urlParts: ['/base', '/nested'] },
  { id: 2, urlParts: ['/base', '/nested', '/nested'] },
  { id: 3, urlParts: ['/base', '/nested', '/:name'] },
  { id: 4, urlParts: ['/base', '/other', '/:name'] },
  { id: 5, urlParts: ['/base', '/:name', '/nested', '/leaf'] },
  { id: 6, urlParts: ['/base', '/:id', '/nested', '/:leaf'] },
  { id: 7, urlParts: ['/base', '/a_name', '/nested', '/leaf'] },
  { id: 8, urlParts: ['/base', '/a_name', '/nested', '/:leaf'] }
];

console.log(matchByUrlParts(['/base']) == null);
console.log(matchByUrlParts(['/base', '/nested']) == 1);
console.log(matchByUrlParts(['/base', '/nested', '/nested']) == 2);
console.log(matchByUrlParts(['/base', '/nested', '/other']) == 3);
console.log(matchByUrlParts(['/base', '/other']) == null);
console.log(matchByUrlParts(['/base', '/other', '/other']) == 4);
console.log(matchByUrlParts(['/base', '/another_name', '/nested', '/leaf']) == 5);
console.log(matchByUrlParts(['/base', '/1234', '/nested', '/variable']) == 6);
console.log(matchByUrlParts(['/base', '/a_name', '/whoops', '/leaf']) == null);
console.log(matchByUrlParts(['/base', '/a_name', '/nested', '/leaf']) == 7);
console.log(matchByUrlParts(['/base', '/a_name', '/nested', '/variable']) == 8);

function matchByUrlParts(urlParts) {
  return 'not implemented';
}

Любая помощь приветствуется.

1
Benny Bottema 27 Май 2017 в 22:22

2 ответа

Лучший ответ

Вы можете построить шаблонную строку для каждого объекта, которая будет иметь один символ для каждой части URL, которая у него есть. Таким образом, длина строки такого шаблона будет равна размеру массива urlPart. Каждый символ будет либо «0», либо «1». Должно быть "1", если соответствующая часть URL является групповым символом.

Пример с учетом этого объекта:

{ id: 6, urlParts: ['/base', '/:id', '/nested', '/:leaf'] },

Соответствующий шаблон будет:

"0101"

... где символы «1» обозначают позиции подстановочных знаков.

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

Поскольку этот шаблон (стоимость), связанный с объектом, не зависит от входных данных, а зависит только от массива objects , вы можете рассчитать его перед обработкой любого ввода и расширить ваши объекты этими шаблонами. Затем вы можете отсортировать объекты по этому шаблону.

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

Эта функция ES6 реализует эту идею:

function preprocess() {
    objects.forEach( o => 
        o.pattern = o.urlParts.map( part => +part.startsWith("/:") ).join('')
    );
    objects.sort( (a, b) => a.pattern.localeCompare(b.pattern) );
}

function matchByUrlParts(input) {
    const found = objects.find( ({id, urlParts, pattern}) => {
        return urlParts.length == input.length
            && input.every( (part, i) => urlParts[i] == part || pattern[i] == "1") 
    });
    return found && found.id || null;
}

var objects =  [
  { id: 1, urlParts: ['/base', '/nested'] },
  { id: 2, urlParts: ['/base', '/nested', '/nested'] },
  { id: 3, urlParts: ['/base', '/nested', '/:name'] },
  { id: 4, urlParts: ['/base', '/other', '/:name'] },
  { id: 5, urlParts: ['/base', '/:name', '/nested', '/leaf'] },
  { id: 6, urlParts: ['/base', '/:id', '/nested', '/:leaf'] },
  { id: 7, urlParts: ['/base', '/a_name', '/nested', '/leaf'] },
  { id: 8, urlParts: ['/base', '/a_name', '/nested', '/:leaf'] }
];

preprocess();

console.log(matchByUrlParts(['/base']) == null);
console.log(matchByUrlParts(['/base', '/nested']) == 1);
console.log(matchByUrlParts(['/base', '/nested', '/nested']) == 2);
console.log(matchByUrlParts(['/base', '/nested', '/other']) == 3);
console.log(matchByUrlParts(['/base', '/other']) == null);
console.log(matchByUrlParts(['/base', '/other', '/other']) == 4);
console.log(matchByUrlParts(['/base', '/another_name', '/nested', '/leaf']) == 5);
console.log(matchByUrlParts(['/base', '/1234', '/nested', '/variable']) == 6);
console.log(matchByUrlParts(['/base', '/a_name', '/whoops', '/leaf']) == null);
console.log(matchByUrlParts(['/base', '/a_name', '/nested', '/leaf']) == 7);
console.log(matchByUrlParts(['/base', '/a_name', '/nested', '/variable']) == 8);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Объяснение

Во время предварительной обработки результат part.startsWith("/:") преобразуется в число с унарным числом +, получая 0 или 1. Функция map возвращает массив таких цифр 0 и 1, которые затем соединены в строку шаблона. Этот шаблон хранится в новом свойстве pattern каждого исходного объекта. Затем эти объекты сортируются по этому новому свойству с помощью функции обратного вызова sort().

Вторая функция тогда довольно проста: она перебирает массив отсортированных объектов с find, проверяя два условия для каждого объекта:

  • У него должно быть столько же частей URL, сколько у входа
  • Каждая часть должна совпадать или соответствующая часть объекта должна быть подстановочным знаком

find прекратит итерацию, когда совпадение будет найдено, и вернет соответствующий объект. Затем функция вернет свойство id этого объекта или null, если совпадений не было.

О шаблоне

Структура нулей и единиц может стать очень длинной. Если бы объекты и входные данные имели бы как 100 частей URL, то это не проблема для этой системы шаблонов: строка может легко иметь длину 100 символов и сравниваться. Однако, если вы реализуете это с помощью чисел, вы столкнетесь с проблемами точности (из-за ограниченной точности с плавающей запятой).

1
trincot 1 Июн 2017 в 17:09

Вот моя собственная версия алгоритма, который, кажется, решает проблему:

function matchByUrlParts(urlParts) {
  let lowest = objects.reduce((lowest, obj) => {
      var cost = obj.urlParts.length == urlParts.length && appraiseRouteCost(obj.urlParts, urlParts);
      var foundMatch = typeof cost == 'number' && !isNaN(cost);
      return (foundMatch && (!lowest || cost < lowest.cost) && {'obj':obj,'cost':cost}) || lowest;
    }, null);
  return lowest && lowest.obj.id;
}

function appraiseRouteCost(a, b, depth = 0) {
  var cost = a[depth] == b[depth] ? 0 : a[depth].startsWith('/:') && 10 ^ (a.length - depth) || undefined;
  return cost + (depth < a.length-1 ? appraiseRouteCost(a, b, depth + 1) : 0);
}

var objects =  [
  { id: 1, urlParts: ['/base', '/nested'] },
  { id: 2, urlParts: ['/base', '/nested', '/nested'] },
  { id: 3, urlParts: ['/base', '/nested', '/:name'] },
  { id: 4, urlParts: ['/base', '/other', '/:name'] },
  { id: 5, urlParts: ['/base', '/:name', '/nested', '/leaf'] },
  { id: 6, urlParts: ['/base', '/:id', '/nested', '/:leaf'] },
  { id: 7, urlParts: ['/base', '/a_name', '/nested', '/leaf'] },
  { id: 8, urlParts: ['/base', '/a_name', '/nested', '/:leaf'] }
];

console.log(matchByUrlParts(['/base']) == null);
console.log(matchByUrlParts(['/base', '/nested']) == 1);
console.log(matchByUrlParts(['/base', '/nested', '/nested']) == 2);
console.log(matchByUrlParts(['/base', '/nested', '/other']) == 3);
console.log(matchByUrlParts(['/base', '/other']) == null);
console.log(matchByUrlParts(['/base', '/other', '/other']) == 4);
console.log(matchByUrlParts(['/base', '/another_name', '/nested', '/leaf']) == 5);
console.log(matchByUrlParts(['/base', '/1234', '/nested', '/variable']) == 6);
console.log(matchByUrlParts(['/base', '/a_name', '/whoops', '/leaf']) == null);
console.log(matchByUrlParts(['/base', '/a_name', '/nested', '/leaf']) == 7);
console.log(matchByUrlParts(['/base', '/a_name', '/nested', '/variable']) == 8);

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

0
Benny Bottema 28 Май 2017 в 14:05