Как извлечь ключи и значения из строки, если значение содержит разделитель между ключами и значениями или разделитель между парами? - PullRequest
5 голосов
/ 05 апреля 2020

Как эффективно извлечь пары ключ-значение из строки в HashMap, когда за

  • key всегда следует :, а затем значение
  • value заканчивается символом ,, за которым следует еще один key (иногда пробел, а затем key)
  • value может содержать , : по всему
  • нет value будет включать в себя любые key
  • порядок key s не фиксирован
  • * известны имена key

Для этих значений ключа пары

key1:value1, key2:this is, some value2, key3:anothe:r val,ue,

Должно получиться это HashMap:

"key1", "value1"
"key2", "this is, some value2"
"key3", "anothe:r val,ue"

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

"key1:value1, key2:this is, some value2, key3:anothe:r val,ue,"
    .split(",")
    .map(|kv| kv.splitn(2, ":").collect::<Vec<&str>>())
    .filter(|vec| vec.len() == 2)
    .map(|vec| (vec[0].trim().into(), vec[1].trim().into()))
    .collect()

Я бы хотел представить список ключей: ["key1", "key2", "key3"] для использования в качестве разделителей

ОБНОВЛЕНИЕ:

Используя ответ @Lucretiel, я придумал:

fn key_value<'a>(keys: &[&str], mut command: &'a str) -> HashMap<&'a str, &'a str> {
    let mut hashmap = HashMap::new();
    loop {
        if let Some(key) = key(&keys, &command) {
            command = &command[key.len() + 1..];

            let value = value(&keys, &command);
            let trim: &[_] = &[',', ' '];
            command = &command[value.len()..].trim_start_matches(trim);

            hashmap.insert(key, value);
        } else {
            break;
        }
    }
    hashmap
}

fn key<'a>(keys: &[&str], command: &'a str) -> Option<&'a str> {
    let regex = format!("^({}):", keys.join("|"));
    let regex = regex::Regex::new(&regex).expect("Invalid regex");
    match regex.shortest_match(&command) {
        Some(position) => Some(&command[..position - 1]),
        None => None,
    }
}

fn value<'a>(keys: &[&str], command: &'a str) -> &'a str {
    let regex = format!(r#",\s*({}):"#, keys.join("|"));
    let regex = regex::Regex::new(&regex).expect("Invalid regex");
    match regex.find(&command) {
        Some(position) => &command[..position.start()],
        None => command,
    }
}

( Детская площадка )

Ответы [ 2 ]

3 голосов
/ 09 апреля 2020

Фактический код для решения этой проблемы нетривиален, но это может быть сделано. Есть много маленьких непростых крайних случаев, в зависимости от того, какие случаи ошибок вы хотите учесть (например, требуется ли, чтобы каждый ключ в вашем известном списке ключей присутствовал во входной строке для разбора? Вы разрешаете дублирование ключей? et c.). Алгоритм basi c выглядит следующим образом:

  • , в то время как список ключей не пуст:
    • найти ключ, который начинает строку, соответствующую ^{key}:. Это текущий ключ.
      • если такого ключа нет, это ошибка; неправильный ввод
    • найти следующий самый ранний ключ в строке, соответствующий ,\s*{key}:. Это следующий ключ.
      • если ключей больше нет, значением этой клавиши является остаток строки
      • в противном случае все содержимое между двумя найденными ключами является текущим значением
    • добавить (текущий ключ, текущее значение) в таблицу ha sh
    • удалить текущий ключ из списка ключей (при условии, что вы не принимаете дубликаты ключей)
    • отрежьте (текущая клавиша, текущее значение) от передней части вашей строки ввода
  • Как только у вас не будет ключей, верните карту ha sh

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

Алгоритм, как описано, выполняется в квадрате c времени , но гипотетически это должно быть приведено к линейному времени, если вы создаете составное регулярное выражение для одновременного поиска каждого ключа:

,\s*(key1|key2|key3|...):

2 голосов
/ 05 апреля 2020

Это не так чисто, как использование итераторов, но вот одна из идей. Чтение ключей и значений затруднено, если вы читаете строку с самого начала из-за необходимости заглядывать вперед, чтобы определить, является ли то, что вы читаете, частью значения или следующим ключом.

Чтение строки в обратном направлении Однако намного проще. Последнее значение - это все после последнего ':'. Последний ключ - это все от последнего ',' до этого до последнего ':'.

Например, мы будем использовать вашу строку.

"key1:value1, key2:this is, some value2, key3:another val,ue,"
                                             ^ the last ':'
"key1:value1, key2:this is, some value2, key3:another val,ue,"
                                       ^ the last ',' before that
"key1:value1, key2:this is, some value2, key3:another val,ue,"
                  ^ the last ':' before that
"key1:value1, key2:this is, some value2, key3:another val,ue,"
            ^ the last ',' before that
"key1:value1, key2:this is, some value2, key3:another val,ue,"
     ^ the last ':' before that

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

Чтобы фактически закодировать это, у нас будет фрагмент, который всегда ссылается на ту часть строки, которая у нас есть. еще не покрыто. На каждом шаге мы найдем последний ':' (или ',') и изменим срез, чтобы указывать перед ним. Использование rsplitn работает довольно хорошо, но я уверен, что есть и другой способ.

fn main() {
    let mut kv = Vec::new();
    let mut slice = "key1:value1, key2:this is, some value2, key3:another val,ue,";
    while !slice.is_empty() {
        let mut split = slice.rsplitn(2, ':');
        // `rsplitn` will always return at least one slice,
        // namely the whole string if there aren't any matches.
        // So we can unwrap here.
        let value = split.next().unwrap().trim();
        // You may want to decide to do something else here.
        // The only way `split.next()` will be `None` is if
        // The input string has incorrect syntax.
        slice = split.next().unwrap_or("");

        let mut split = slice.rsplitn(2, ',');
        // similar reasoning here
        let key = split.next().unwrap().trim();
        slice = split.next().unwrap_or("");

        kv.push((key, value));
    }
    println!("{:?}", kv);
}

(игровая площадка)

Всего одна заметка. Код выше подсчитывает любые запятые как часть последнего значения. Если вы не хотите этого, вы можете сделать проверку. Не забудьте сначала trim строку (и это может стоить сделать в любом случае)! В будущем strip_suffix будет хорошо работать здесь. На данный момент, str::ends_with должно подойти.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...