Динамическое создание регулярного выражения из DateFormat - PullRequest
0 голосов
/ 18 сентября 2018

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

Однако даты в этом тексте локализованы.И приложение должно быть в состоянии адаптироваться к различным локализованным датам.К счастью, я могу определить правильный формат даты для текущей локали, используя DateFormat.getDateInstance(SHORT, locale).Я могу получить образец даты от этого.Но как мне динамически преобразовать его в регулярное выражение?

Регулярному выражению не нужно было бы выполнять углубленную проверку формата (високосные годы, правильное количество дней в месяце и т. Д..), Я уже могу быть уверен, что данные предоставлены в действительном формате.Дата просто должна быть идентифицирована (как, например, регулярное выражение должно быть в состоянии определить начальный и конечный индекс того, где находится дата).

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

Я не могу использовать DateFormat.parse(...), так как сначала мне нужно определить дату, и могуне извлекайте его напрямую.

Ответы [ 3 ]

0 голосов
/ 18 сентября 2018

Поскольку вы выполняете getDateInstance(SHORT, locale), с акцентом на Date и SHORT, шаблоны довольно ограничены, поэтому подойдет следующий код:

public static String dateFormatToRegex(Locale locale) {
    StringBuilder regex = new StringBuilder();
    String fmt = ((SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, locale)).toPattern();
    for (Matcher m = Pattern.compile("[^a-zA-Z]+|([a-zA-Z])\\1*").matcher(fmt); m.find(); ) {
        String part = m.group();
        if (m.start(1) == -1) { // Not letter(s): Literal text
            regex.append(Pattern.quote(part));
        } else {
            switch (part.charAt(0)) {
                case 'G': // Era designator
                    regex.append("\\p{L}+");
                    break;
                case 'y': // Year
                    regex.append("\\d{1,4}");
                    break;
                case 'M': // Month in year
                    if (part.length() > 2)
                        throw new UnsupportedOperationException("Date format part: " + part);
                    regex.append("(?:1[0-2]|0?[1-9])");
                    break;
                case 'd': // Day in month
                    regex.append("(?:3[01]|[12][0-9]|0?[1-9])");
                    break;
                default:
                    throw new UnsupportedOperationException("Date format part: " + part);
            }
        }
    }
    return regex.toString();
}

Чтобы увидеть, что вы используете для регулярного выраженияПолучу для разных локалей:

Locale[] locales = Locale.getAvailableLocales();
Arrays.sort(locales, Comparator.comparing(Locale::toLanguageTag));
Map<String, List<String>> fmtLocales = new TreeMap<>();
for (Locale locale : locales) {
    String fmt = ((SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, locale)).toPattern();
    fmtLocales.computeIfAbsent(fmt, k -> new ArrayList<>()).add(locale.toLanguageTag());
}
fmtLocales.forEach((k, v) -> System.out.println(dateFormatToRegex(Locale.forLanguageTag(v.get(0))) + "   " + v));

Выход

\p{L}+\d{1,4}\Q.\E(?:0[1-9]|1[0-2])\Q.\E(?:0[1-9]|[12][0-9]|3[01])   [ja-JP-u-ca-japanese-x-lvariant-JP]
(?:0[1-9]|1[0-2])\Q/\E(?:0[1-9]|[12][0-9]|3[01])\Q/\E\d{1,4}   [brx, brx-IN, chr, chr-US, ee, ee-GH, ee-TG, en, en-AS, en-BI, en-GU, en-MH, en-MP, en-PR, en-UM, en-US, en-US-POSIX, en-VI, fil, fil-PH, ks, ks-IN, ug, ug-CN, zu, zu-ZA]
(?:0[1-9]|1[0-2])\Q/\E(?:0[1-9]|[12][0-9]|3[01])\Q/\E\d{1,4}   [es-PA, es-PR]
(?:0[1-9]|[12][0-9]|3[01])\Q-\E(?:0[1-9]|1[0-2])\Q-\E\d{1,4}   [or, or-IN]
(?:0[1-9]|[12][0-9]|3[01])\Q. \E(?:0[1-9]|1[0-2])\Q. \E\d{1,4}   [ksh, ksh-DE]
(?:0[1-9]|[12][0-9]|3[01])\Q. \E(?:0[1-9]|1[0-2])\Q. \E\d{1,4}   [sl, sl-SI]
(?:0[1-9]|[12][0-9]|3[01])\Q.\E(?:0[1-9]|1[0-2])\Q.\E\d{1,4}   [fi, fi-FI, he, he-IL, is, is-IS]
(?:0[1-9]|[12][0-9]|3[01])\Q.\E(?:0[1-9]|1[0-2])\Q.\E\d{1,4}   [be, be-BY, dsb, dsb-DE, hsb, hsb-DE, sk, sk-SK, sq, sq-AL, sq-MK, sq-XK]
(?:0[1-9]|[12][0-9]|3[01])\Q.\E(?:0[1-9]|1[0-2])\Q.\E\d{1,4}\Q.\E   [bs-Cyrl, bs-Cyrl-BA, sr, sr-CS, sr-Cyrl, sr-Cyrl-BA, sr-Cyrl-ME, sr-Cyrl-RS, sr-Cyrl-XK, sr-Latn, sr-Latn-BA, sr-Latn-ME, sr-Latn-RS, sr-Latn-XK, sr-ME, sr-RS]
(?:0[1-9]|[12][0-9]|3[01])\Q.\E(?:0[1-9]|1[0-2])\Q.\E\d{1,4}   [tr, tr-CY, tr-TR]
(?:0[1-9]|[12][0-9]|3[01])\Q.\E(?:0[1-9]|1[0-2])\Q.\E\d{1,4}\Q 'г'.\E   [bg, bg-BG]
(?:0[1-9]|[12][0-9]|3[01])\Q/\E(?:0[1-9]|1[0-2])\Q/\E\d{1,4}   [agq, agq-CM, bas, bas-CM, bm, bm-ML, dje, dje-NE, dua, dua-CM, dyo, dyo-SN, en-HK, en-ZW, ewo, ewo-CM, ff, ff-CM, ff-GN, ff-MR, ff-SN, kab, kab-DZ, kea, kea-CV, khq, khq-ML, ksf, ksf-CM, ln, ln-AO, ln-CD, ln-CF, ln-CG, lo, lo-LA, lu, lu-CD, mfe, mfe-MU, mg, mg-MG, mua, mua-CM, nmg, nmg-CM, rn, rn-BI, seh, seh-MZ, ses, ses-ML, sg, sg-CF, shi, shi-Latn, shi-Latn-MA, shi-MA, shi-Tfng, shi-Tfng-MA, sw-CD, twq, twq-NE, yav, yav-CM, zgh, zgh-MA, zh-HK, zh-Hant-HK, zh-Hant-MO]
(?:0[1-9]|[12][0-9]|3[01])\Q/\E(?:0[1-9]|1[0-2])\Q/\E\d{1,4}   [ast, ast-ES, bn, bn-BD, bn-IN, ca, ca-AD, ca-ES, ca-ES-VALENCIA, ca-FR, ca-IT, el, el-CY, el-GR, en-AU, en-SG, es, es-419, es-AR, es-BO, es-BR, es-CR, es-CU, es-DO, es-EA, es-EC, es-ES, es-GQ, es-HN, es-IC, es-NI, es-PH, es-PY, es-SV, es-US, es-UY, es-VE, gu, gu-IN, ha, ha-GH, ha-NE, ha-NG, haw, haw-US, hi, hi-IN, km, km-KH, kn, kn-IN, ml, ml-IN, mr, mr-IN, pa, pa-Guru, pa-Guru-IN, pa-IN, pa-PK, ta, ta-IN, ta-LK, ta-MY, ta-SG, th, th-TH, to, to-TO, ur, ur-IN, ur-PK, zh-Hans-HK, zh-Hans-MO]
(?:0[1-9]|[12][0-9]|3[01])\Q/\E(?:0[1-9]|1[0-2])\Q/\E\d{1,4}   [th-TH-u-nu-thai-x-lvariant-TH]
(?:0[1-9]|[12][0-9]|3[01])\Q/\E(?:0[1-9]|1[0-2])\Q/\E\d{1,4}   [nus, nus-SS]
(?:0[1-9]|[12][0-9]|3[01])\Q/\E(?:0[1-9]|1[0-2])\Q/\E\d{1,4}   [en-NZ, es-CO, es-GT, es-PE, fr-BE, ms, ms-BN, ms-MY, ms-SG, nl-BE]
(?:0[1-9]|[12][0-9]|3[01])\Q-\E(?:0[1-9]|1[0-2])\Q-\E\d{1,4}   [sv-FI]
(?:0[1-9]|[12][0-9]|3[01])\Q-\E(?:0[1-9]|1[0-2])\Q-\E\d{1,4}   [es-CL, fy, fy-NL, my, my-MM, nl, nl-AW, nl-BQ, nl-CW, nl-NL, nl-SR, nl-SX, rm, rm-CH, te, te-IN]
(?:0[1-9]|[12][0-9]|3[01])\Q.\E(?:0[1-9]|1[0-2])\Q.\E\d{1,4}   [mk, mk-MK]
(?:0[1-9]|[12][0-9]|3[01])\Q.\E(?:0[1-9]|1[0-2])\Q.\E\d{1,4}   [nb, nb-NO, nb-SJ, nn, nn-NO, nn-NO, no, no-NO, pl, pl-PL, ro, ro-MD, ro-RO, tk, tk-TM]
(?:0[1-9]|[12][0-9]|3[01])\Q.\E(?:0[1-9]|1[0-2])\Q.\E\d{1,4}\Q.\E   [hr, hr-BA, hr-HR]
(?:0[1-9]|[12][0-9]|3[01])\Q.\E(?:0[1-9]|1[0-2])\Q.\E\d{1,4}   [az, az-AZ, az-Cyrl, az-Cyrl-AZ, az-Latn, az-Latn-AZ, cs, cs-CZ, de, de-AT, de-BE, de-CH, de-DE, de-LI, de-LU, et, et-EE, fo, fo-DK, fo-FO, fr-CH, gsw, gsw-CH, gsw-FR, gsw-LI, hy, hy-AM, it-CH, ka, ka-GE, kk, kk-KZ, ky, ky-KG, lb, lb-LU, lv, lv-LV, os, os-GE, os-RU, ru, ru-BY, ru-KG, ru-KZ, ru-MD, ru-RU, ru-UA, uk, uk-UA]
(?:0[1-9]|[12][0-9]|3[01])\Q.\E(?:0[1-9]|1[0-2])\Q.\E\d{1,4}\Q.\E   [bs, bs-BA, bs-Latn, bs-Latn-BA]
(?:0[1-9]|[12][0-9]|3[01])\Q/\E(?:0[1-9]|1[0-2])\Q \E\d{1,4}   [kkj, kkj-CM]
(?:0[1-9]|[12][0-9]|3[01])\Q/\E(?:0[1-9]|1[0-2])\Q/\E\d{1,4}   [am, am-ET, asa, asa-TZ, bem, bem-ZM, bez, bez-TZ, cgg, cgg-UG, da, da-DK, da-GL, dav, dav-KE, ebu, ebu-KE, en-001, en-150, en-AG, en-AI, en-AT, en-BB, en-BM, en-BS, en-CC, en-CH, en-CK, en-CM, en-CX, en-CY, en-DE, en-DG, en-DK, en-DM, en-ER, en-FI, en-FJ, en-FK, en-FM, en-GB, en-GD, en-GG, en-GH, en-GI, en-GM, en-GY, en-IE, en-IL, en-IM, en-IO, en-JE, en-JM, en-KE, en-KI, en-KN, en-KY, en-LC, en-LR, en-LS, en-MG, en-MO, en-MS, en-MT, en-MU, en-MW, en-MY, en-NA, en-NF, en-NG, en-NL, en-NR, en-NU, en-PG, en-PH, en-PK, en-PN, en-PW, en-RW, en-SB, en-SC, en-SD, en-SH, en-SI, en-SL, en-SS, en-SX, en-SZ, en-TC, en-TK, en-TO, en-TT, en-TV, en-TZ, en-UG, en-VC, en-VG, en-VU, en-WS, en-ZM, fr, fr-BF, fr-BI, fr-BJ, fr-BL, fr-CD, fr-CF, fr-CG, fr-CI, fr-CM, fr-DJ, fr-DZ, fr-FR, fr-GA, fr-GF, fr-GN, fr-GP, fr-GQ, fr-HT, fr-KM, fr-LU, fr-MA, fr-MC, fr-MF, fr-MG, fr-ML, fr-MQ, fr-MR, fr-MU, fr-NC, fr-NE, fr-PF, fr-PM, fr-RE, fr-RW, fr-SC, fr-SN, fr-SY, fr-TD, fr-TG, fr-TN, fr-VU, fr-WF, fr-YT, ga, ga-IE, gd, gd-GB, guz, guz-KE, ig, ig-NG, jmc, jmc-TZ, kam, kam-KE, kde, kde-TZ, ki, ki-KE, kln, kln-KE, ksb, ksb-TZ, lag, lag-TZ, lg, lg-UG, luo, luo-KE, luy, luy-KE, mas, mas-KE, mas-TZ, mer, mer-KE, mgh, mgh-MZ, mt, mt-MT, naq, naq-NA, nd, nd-ZW, nyn, nyn-UG, pa-Arab, pa-Arab-PK, qu, qu-BO, qu-EC, qu-PE, rof, rof-TZ, rwk, rwk-TZ, saq, saq-KE, sbp, sbp-TZ, sn, sn-ZW, sw, sw-KE, sw-TZ, sw-UG, teo, teo-KE, teo-UG, tzm, tzm-MA, vai, vai-LR, vai-Latn, vai-Latn-LR, vai-Vaii, vai-Vaii-LR, vi, vi-VN, vun, vun-TZ, xog, xog-UG, yo, yo-BJ, yo-NG]
(?:0[1-9]|[12][0-9]|3[01])\Q/\E(?:0[1-9]|1[0-2])\Q/\E\d{1,4}   [cy, cy-GB, en-BE, en-BW, en-BZ, en-IN, es-MX, fur, fur-IT, gl, gl-ES, id, id-ID, it, it-IT, it-SM, nnh, nnh-CM, om, om-ET, om-KE, pt, pt-AO, pt-BR, pt-CH, pt-CV, pt-GQ, pt-GW, pt-LU, pt-MO, pt-MZ, pt-PT, pt-ST, pt-TL, so, so-DJ, so-ET, so-KE, so-SO, ti, ti-ER, ti-ET, uz, uz-AF, uz-Cyrl, uz-Cyrl-UZ, uz-Latn, uz-Latn-UZ, uz-UZ, yi, yi-001, zh-Hans-SG, zh-SG]
(?:0[1-9]|[12][0-9]|3[01])\Q‏/\E(?:0[1-9]|1[0-2])\Q‏/\E\d{1,4}   [ar, ar-001, ar-AE, ar-BH, ar-DJ, ar-DZ, ar-EG, ar-EH, ar-ER, ar-IL, ar-IQ, ar-JO, ar-KM, ar-KW, ar-LB, ar-LY, ar-MA, ar-MR, ar-OM, ar-PS, ar-QA, ar-SA, ar-SD, ar-SO, ar-SS, ar-SY, ar-TD, ar-TN, ar-YE]
\d{1,4}\Q-\E(?:0[1-9]|1[0-2])\Q-\E(?:0[1-9]|[12][0-9]|3[01])   [af, af-NA, af-ZA, as, as-IN, bo, bo-CN, bo-IN, br, br-FR, ce, ce-RU, ckb, ckb-IQ, ckb-IR, cu, cu-RU, dz, dz-BT, en-CA, en-SE, gv, gv-IM, ii, ii-CN, jgo, jgo-CM, kl, kl-GL, kok, kok-IN, kw, kw-GB, lkt, lkt-US, lrc, lrc-IQ, lrc-IR, lt, lt-LT, mgo, mgo-CM, mn, mn-MN, mzn, mzn-IR, ne, ne-IN, ne-NP, prg, prg-001, se, se-FI, se-NO, se-SE, si, si-LK, smn, smn-FI, sv, sv-AX, sv-SE, und, uz-Arab, uz-Arab-AF, vo, vo-001, wae, wae-CH]
\d{1,4}\Q. \E(?:0[1-9]|1[0-2])\Q. \E(?:0[1-9]|[12][0-9]|3[01])\Q.\E   [hu, hu-HU]
\d{1,4}\Q/\E(?:0[1-9]|1[0-2])\Q/\E(?:0[1-9]|[12][0-9]|3[01])   [fa, fa-AF, fa-IR, ps, ps-AF, yue, yue-HK, zh, zh-CN, zh-Hans, zh-Hans-CN, zh-Hant, zh-Hant-TW, zh-TW]
\d{1,4}\Q/\E(?:0[1-9]|1[0-2])\Q/\E(?:0[1-9]|[12][0-9]|3[01])   [en-ZA, eu, eu-ES, ja, ja-JP]
\d{1,4}\Q-\E(?:0[1-9]|1[0-2])\Q-\E(?:0[1-9]|[12][0-9]|3[01])   [eo, eo-001, fr-CA, sr-BA]
\d{1,4}\Q. \E(?:0[1-9]|1[0-2])\Q. \E(?:0[1-9]|[12][0-9]|3[01])\Q.\E   [ko, ko-KP, ko-KR]
\d{1,4}\Q/\E(?:0[1-9]|1[0-2])\Q/\E(?:0[1-9]|[12][0-9]|3[01])   [sah, sah-RU]
\d{1,4}\Q/\E(?:0[1-9]|1[0-2])\Q/\E(?:0[1-9]|[12][0-9]|3[01])   [ak, ak-GH, rw, rw-RW]
0 голосов
/ 19 сентября 2018

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

    Locale loc = Locale.forLanguageTag("en-AS");
    DateTimeFormatter dateFormatter
            = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(loc);

    String mixed = "09/03/18Some data06/29/18Some other data04/27/18A third piece of data";
    // Check that the string starts with a date
    ParsePosition pos = new ParsePosition(0);
    LocalDate.from(dateFormatter.parse(mixed, pos));
    int dataStartIndex = pos.getIndex();
    System.out.println("Date: " + mixed.substring(0, dataStartIndex));
    int candidateDateStartIndex = dataStartIndex;
    while (candidateDateStartIndex < mixed.length()) {
        try {
            pos.setIndex(candidateDateStartIndex);
            LocalDate.from(dateFormatter.parse(mixed, pos));
            // Date found
            System.out.println("Data: " 
                    + mixed.substring(dataStartIndex, candidateDateStartIndex));
            dataStartIndex = pos.getIndex();
            System.out.println("Date: "
                    + mixed.substring(candidateDateStartIndex, dataStartIndex));
            candidateDateStartIndex = dataStartIndex;
        } catch (DateTimeException dte) {
            // No date here; try next
            candidateDateStartIndex++;
            pos.setErrorIndex(-1); // Clear error
        }
    }
    System.out.println("Data: " + mixed.substring(dataStartIndex, mixed.length()));

Вывод этого фрагмента:

Date: 09/03/18
Data: Some data
Date: 06/29/18
Data: Some other data
Date: 04/27/18
Data: A third piece of data

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

Именно потому, что я представляю это для более широкой аудитории, я использую java.time, современный Java-интерфейс даты и времени.Если ваши данные изначально были записаны с DateFormat, вы можете заменить этот класс на приведенный выше код.Я надеюсь, что вы сделаете это в этом случае.

0 голосов
/ 18 сентября 2018

То, что вы спрашиваете, является действительно сложным, но это не невозможно - просто вероятно много сотен строк кода, прежде чем вы закончите.Я действительно не уверен, что это тот путь, по которому вы хотите идти - честно, если вы уже знаете, в каком формате находится дата, вам, вероятно, следует просто parse() указать ее - но, скажем, ради аргумента, что вы действительно do хотите превратить шаблон даты, такой как YYYY-mm-dd HH:mm:ss, в регулярное выражение, которое может соответствовать датам в этом формате.

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

Лексический анализ или токенизация - это акт взломавведите строку в свой компонент tokens , чтобы вместо массива символов она стала последовательностью перечисляемых значений или объектов: так что в предыдущем примере вы получили бы массив или список, подобный этому: [YYYY, Hyphen, mm, Hyphen, dd, Space, HH, Colon, mm, Colon, ss].Этот вид токенизации часто выполняется с большим конечным автоматом, и вы можете найти где-нибудь код с открытым исходным кодом (возможно, часть исходного кода Android), который уже это делает.Если нет, вам придется прочитать каждую букву, подсчитать, сколько из этой буквы есть, и выбрать соответствующее значение enum для добавления в растущий список токенов.

Как только вы получите последовательность токеновэлементы, следующий шаг должен преобразовать каждый в кусок регулярного выражения, который действителен для текущей локализации.Это, вероятно, гигантский оператор switch внутри цикла над токенами, и поэтому он превращает значение перечисления YYYY в строковый фрагмент "[0-9]{4}" или значение перечисления mmm в большой кусок строки регулярного выражения, соответствующийвсе названия месяцев в текущей локали ("jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec").Это, очевидно, подразумевает, что вы извлекаете все данные для данной локали, так что вы можете сделать фрагменты регулярного выражения из его слов.

Наконец, вы можете объединить все биты регулярного выражения, заключив каждый бит в скобки, чтобыправильность приоритета и, наконец, Pattern.compile() всей строки.Не забудьте использовать тест без учета регистра.

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

Это проект с половиной, но это то, что можно построить, если вы действительно действительнона самом деле нужно , чтобы оно работало точно так, как вы описали.

Но, опять же, на вашем месте я бы придерживался того, что уже существует: если вы уже знаете, в какой локали вы находитесь(или даже если вы этого не сделаете), метод parse() уже не только делает для вас лексический анализ и проверку ввода - и не только уже написан!- но он также создает пригодный для использования объект даты!

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