#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
(. ?)
будет соответствовать и захватывать текстовый сегмент({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}"
));
Комментарии:
1. Интересно, спасибо за ответ и информацию!