Разделение строки с помощью маркеров в js

#javascript #typescript

#javascript #typescript

Вопрос:

У меня есть строка с некоторыми маркерами:

 'This is {startMarker} the string {endMarker} for {startMarker} example. {endMarker}'
  

Мне нужно разобрать ее в массив, например :

 [
    {marker: false, value: 'This is'},
    {marker: true,  value: 'the string'},
    {marker: false, value: 'for'},
    {marker: true, value:  'example.'}
]
  

Поэтому сохраняйте порядок предложений, но добавляйте информацию о маркере.
Есть идеи, как я могу этого добиться?
Спасибо

Комментарии:

1. Нам проще помочь с устранением неполадок, чем разрабатывать целые программы. Не могли бы вы пояснить, какие шаги вы пытались предпринять для решения этой проблемы самостоятельно и с какими проблемами вы столкнулись?

2. Что я сделал, так это разделил текст до первого {startMarker}, затем добавил его в массив с помощью marker:false , затем разделил до следующего {endMarker} и добавил его с помощью marker:true и т.д. до конца. Но мне интересно, есть ли более разумное решение

3. Это всегда <text><marker<text><marker><end> ? Может ли у вас быть текст до конца ввода: <text><marker<text><marker><text><end> ?

Ответ №1:

Это должно сработать

 const my_str = 'This is {startMarker} the string {endMarker} for {startMarker} example.{endMarker}';

const my_arr = my_str.split('{endMarker}').reduce((acc, s) =>
                  s.split('{startMarker}').map((a,i) =>
                      a amp;amp; acc.push({
                        marker: i ? true : false,
                        value: a.trim()}))
                      amp;amp; acc,[]);
     
console.log(my_arr)  

Ответ №2:

Только потому, что вы новый участник…

 interface MarkedString {
   marker: boolean
   value: string
}

function markString(text: string): MarkedString[] {
   let match: RegExpExecArray | null

   const firstMatch = text.slice(0, text.indexOf('{') - 1)
   
   const array: MarkedString[] = firstMatch.length > 0 ? [
      { marker: false, value: firstMatch  }
   ] : []
   
   while ((match = /{(. ?)}/g.exec(text)) !== null) {
      if (!match) break
   
      const marker = match[0].slice(1, match[0].slice(1).indexOf('}')   1)
   
      const markerEnd = match.index   match[0].length
   
      const value = text.slice(markerEnd ,markerEnd   text.slice(markerEnd).indexOf('{')).trim()
   
      if (value === '') break
   
      if (marker === 'startMarker') {
         array.push({ marker: true, value })
      } else if (marker === 'endMarker') {
         array.push({ marker: false, value })
      }
   
      text = text.slice(markerEnd   value.length   1)
   }
   
   return array
}
  

Ответ №3:

 const escapeRegex = s => s.replace(/[.* -?^${}()|[]\]/g, "\$amp;");

const extract = (start, end, str) => Array.from(
  str.matchAll(`(. ?)(${escapeRegex(start)}|${escapeRegex(end)}|$)`),
  ([, text, mark]) => ({
    marker: mark === end,
    value: text.trim()
  })
);

console.log(extract(
  "{startMarker}",
  "{endMarker}",
  "This is {startMarker} the string {endMarker} for {startMarker} example. {endMarker}"
));  

Ссылка на игровую площадку

Объяснение

Регулярное выражение

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

 This is {startMarker} the string {endMarker} 
^______^^___________^^__________^^_________^
| text       mark   ||   text        mark  |
^___________________^^_____________________^
       section               section
  

Текст станет value объектом результата, сегмент маркера можно проверить, предназначен ли он {endMarker} для создания true или false для объекта результата.

Итак, если бы мы могли правильно извлечь сегменты и разделы, результат:

 result = {
  marker: marker === "{endMarker}",
  value: text.trim()
}
  

Регулярное выражение, которое сделает это за нас, это:

 /(. ?)({startMarker}|{endMarker}|$)/g
  

Смотрите на Regex101

  • (. ?) будет соответствовать и захватывать текстовый сегмент
  • ({startMarker}|{endMarker}|$) сопоставит и извлечет маркер в конце текстового сегмента. Он также соответствует концу строки на случай, если после последней метки будет больше текста, например, если у вас есть for {startMarker} example. {endMarker} more text here

Генерация

Чтобы быть более общим, мы можем взять любую строку для начального и конечного маркера, а затем экранировать их, чтобы убедиться, что они совпадают буквально, даже если в них есть мета-символы, такие как . или * . Я взял escape-функцию из MDN и для краткости преобразовал ее в функцию со стрелкой:

 const escapeRegex = s => s.replace(/[.* -?^${}()|[]\]/g, "\$amp;");
  

Таким образом, мы можем принимать start и end как строки и генерировать регулярное выражение с помощью RegExp конструктора:

 const escapeRegex = s => s.replace(/[.* -?^${}()|[]\]/g, "\$amp;");

const start = "{startMarker}";
const end = "{endMarker}";

const regex = new RegExp(`(. ?)(${escapeRegex(start)}|${escapeRegex(end)}|$)`, "g");

console.log(regex.toString());  

Сопоставление

String#matchAll Метод создаст итератор, который содержит все совпадения из этого регулярного выражения, примененного к строке.

 const escapeRegex = s => s.replace(/[.* -?^${}()|[]\]/g, "\$amp;");

const extract = (start, end, str) => {
  const sequence = str.matchAll(`(. ?)(${escapeRegex(start)}|${escapeRegex(end)}|$)`);
  
  for(const result of sequence) {
    console.log(result);
  }
};

extract(
  "{startMarker}",
  "{endMarker}",
  "This is {startMarker} the string {endMarker} for {startMarker} example. {endMarker}"
);  

.matchAll() Метод принимает строку в качестве параметра и автоматически преобразует ее в регулярное выражение с помощью RegExp конструктора, а затем автоматически добавляет глобальный флаг. Однако в настоящее время TypeScript, похоже, не допускает этого — ввод для метода разрешает только RegExp объект, поэтому только для TypeScript (пока ввод не будет исправлен) вам нужно вызвать

 str.matchAll(new RegExp(`(. ?)(${escapeRegex(start)}|${escapeRegex(end)}|$)`, "g"))
  

Преобразовать в массив

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

Поскольку мы получаем результаты сопоставления регулярных выражений, мы можем напрямую преобразовать их в нужные элементы с помощью этой функции:

 result => {
  const match = result[1];
  const marker = result[2];

  return {
    marker: marker === end,
    value: match.trim()
  };
}
  

Что дает нам более подробную версию:

 const escapeRegex = s => s.replace(/[.* -?^${}()|[]\]/g, "\$amp;");

const extract = (start, end, str) => {
  return Array.from(
    str.matchAll(`(. ?)(${escapeRegex(start)}|${escapeRegex(end)}|$)`),
    result => {
      const match = result[1];
      const marker = result[2];

      return {
        marker: marker === end,
        value: match.trim()
      };
    }
  );
}

console.log(extract(
  "{startMarker}",
  "{endMarker}",
  "This is {startMarker} the string {endMarker} for {startMarker} example. {endMarker}"
));  

Ссылка на игровую площадку

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

 ([, text, mark]) => ({
  marker: mark === end,
  value: text.trim()
})
  

Который, наконец, дает нам начальный бит кода сверху (снова включен, чтобы избежать прокрутки назад):

 const escapeRegex = s => s.replace(/[.* -?^${}()|[]\]/g, "\$amp;");

const extract = (start, end, str) => Array.from(
  str.matchAll(`(. ?)(${escapeRegex(start)}|${escapeRegex(end)}|$)`),
  ([, text, mark]) => ({
    marker: mark === end,
    value: text.trim()
  })
);

console.log(extract(
  "{startMarker}",
  "{endMarker}",
  "This is {startMarker} the string {endMarker} for {startMarker} example. {endMarker}"
));  

Ссылка на игровую площадку

Заключительное замечание о совместимости с ES2020

String#matchAll взято из спецификаций ES2020. Если вы в настоящее время не нацелены на это и не хотите, вы можете очень легко создать свою собственную версию, используя функцию генератора, которая будет работать очень похоже:

 function* matchAll(pattern, text) {
  const regex = typeof pattern === "string"
    ? new RegExp(pattern, "g")  //convert to global regex
    : new RegExp(pattern);      //or make a copy of the regex object to avoid mutating the input
    
  let resu<
  while(result = regex.exec(text)) //apply `regex.exec` repeatedly
    yield resu<                  //and produce each result from the iterator
}
  

Единственным заметным упущением здесь является то String#matchAll , что при передаче неглобального объекта регулярных выражений будет выдана ошибка. Это все еще может быть реализовано, но я выбрал немного более короткую реализацию для иллюстрации.

Используя пользовательский matchAll , вы можете настроить таргетинг на версии до ES2020 без необходимости полизаполнения

 const escapeRegex = s => s.replace(/[.* -?^${}()|[]\]/g, "\$amp;");

function* matchAll(pattern, text) {
  const regex = typeof pattern === "string"
    ? new RegExp(pattern, "g")
    : new RegExp(pattern);
    
  let resu<
  while(result = regex.exec(text))
    yield resu<
}

const extract = (start, end, str) => Array.from(
  matchAll(`(. ?)(${escapeRegex(start)}|${escapeRegex(end)}|$)`, str),
  ([, text, mark]) => ({
    marker: mark === end,
    value: text.trim()
  })
);

console.log(extract(
  "{startMarker}",
  "{endMarker}",
  "This is {startMarker} the string {endMarker} for {startMarker} example. {endMarker}"
));  

Ссылка на игровую площадку с целью, установленной на ES2015

Комментарии:

1. Интересно, спасибо за ответ и информацию!