Java — строка общего назначения для парсера LocalDateTime

У меня есть несколько общих методов синтаксического анализатора LocalDateTime, которые я написал для обработки дат, проходящих между несколькими устаревшими конвейерами обработки на основе XML и переходящими в современные приложения Spring Boot, Mongo. Поскольку и устаревшие, и современные приложения высечены из камня, я застрял в узком преобразовании устаревших дат String в LocalDateTime. Тем не менее, я хотел представить его на рассмотрение и, возможно, улучшить код. Существуют различные устаревшие строковые даты:

    // 200701     - 6  yyyyMM
    // 032007     - 6  MMyyyy
    // 2007-01    - 7  yyyy-MM
    // 03-2007    - 7  MM-yyyy
    // 03012007   - 8  MMddyyyy
    // 20070301   - 8  yyyyMMdd
    // 03-01-2007 - 10 MM-dd-yyyy
    // 2007-03-01 - 10 MM-dd-yyyy

И методы, которые я придумал, следующие: Основной метод — это parseDate (), я просто передаю устаревшую String Date и в основном заставляю ее вычислять длину, а затем пытаюсь преобразовать ее в LocalDateTime. Он работает, и все тесты JUnit проходят.

public LocalDateTime    parseDate(String dateString) {
    checkArgument(!Strings.isNullOrEmpty(dateString), "Date [" + dateString + "] is empty of NULL!");
    LOG.debug("parseDate.................................Date [{}]", dateString);
    LocalDateTime   ldateResults = null;
    dateString = dateString.replace("/","-");

    switch (dateString.length()) 
    {
        case 6:
            try { ldateResults = getLocalDateTimeFromString ( dateString, "MMyyyy" ); }
            catch ( Exception ltwoXcp ) {
                LOG.warn("Date [{}] failed with format [MMyyyy], trying another", dateString);
                try { ldateResults = getLocalDateTimeFromString ( dateString, "yyyyMM" ); }
                catch ( Exception loneXcp ) {
                    LOG.warn("Date [{}] failed with format [yyyyMM], trying another", dateString);
                }
            }
            break;
        case 7:
            if ( dateString.indexOf("-") > 3 ) {
                try { ldateResults = getLocalDateTimeFromString ( dateString, "yyyy-MM" ); }
                catch ( Exception loneXcp ) {
                    LOG.warn("Date [{}] failed with format [yyyy-MM], trying another", dateString);
                }
                try { ldateResults = getLocalDateTimeFromString ( dateString, "MM-yyyy" ); }
                catch ( Exception ltwoXcp ) {
                    LOG.warn("Date [{}] failed with format [MM-yyyy], trying another", dateString);
                }
            }
            break;
        case 8:
            try { ldateResults = getLocalDateTimeFromString ( dateString, "MMddyyyy" ); }
            catch ( Exception loneXcp ) {
                LOG.warn("Date [{}] failed with format [MMddyyyy], trying another", dateString);
                try { ldateResults = getLocalDateTimeFromString ( dateString, "yyyyMMdd" ); }
                catch ( Exception ltwoXcp ) {
                    LOG.warn("Date [{}] failed with format [yyyyMMdd], trying another", dateString);
                }
            }
            break;
        case 10:
            try { ldateResults = getLocalDateTimeFromString ( dateString, "MM-dd-yyyy" ); }
            catch ( Exception loneXcp ) {
                LOG.warn("Date [{}] failed with format [MM-dd-yyyy], trying another", dateString);
                try { ldateResults = getLocalDateTimeFromString ( dateString, "yyyy-MM-dd" ); }
                catch ( Exception ltwoXcp ) {
                    LOG.warn("Date [{}] failed with format [yyyy-MM-dd], trying another", dateString);
                }
            }
            break;
    }
    
    return ldateResults;
    
}
public LocalDateTime getLocalDateTimeFromString(String dateString, String dateFormat) {
    DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(dateFormat);
    if ( dateFormat.length() == 6 ) {
        LocalDate localDate = YearMonth.parse(dateString, dateFormatter).atDay(1);
        return localDate.atStartOfDay();
    }
    return LocalDate.parse(dateString, dateFormatter).atStartOfDay();
}
public String getStringFromLocalDateTime(LocalDateTime date, String dateFormat) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
    return formatter.format(date);
}

Вот несколько запускаемых тестов JUnit:

@Test
public void parseDateReturnsCorrectyyyyMM() {
    String dateString="200703";
    LocalDateTime ldtValue = null;
    try {
        ldtValue = dateUtils.parseDate(dateString);
    } catch (Exception e) {
        e.printStackTrace();
        fail(e.getMessage());
    }
    if ( ldtValue != null ) {
        dateString = dateUtils.getStringFromLocalDateTime(ldtValue,"MM/dd/yyyy");
    }
    
    assertEquals("03/01/2007", dateString);
}

@Test
public void parseDateReturnsCorrectMMyyyy() {
    String dateString="032007";
    LocalDateTime ldtValue = null;
    try {
        ldtValue = dateUtils.parseDate(dateString);
    } catch (Exception e) {
        e.printStackTrace();
        fail(e.getMessage());
    }
    if ( ldtValue != null ) {
        dateString = dateUtils.getStringFromLocalDateTime(ldtValue,"MM/dd/yyyy");
    }
    
    assertEquals("03/01/2007", dateString);
}

@Test
public void parseDateReturnsCorrectyyyyMMdd() {
    String dateString="20070301";
    LocalDateTime ldtValue = null;
    try {
        ldtValue = dateUtils.parseDate(dateString);
    } catch (Exception e) {
        e.printStackTrace();
        fail(e.getMessage());
    }
    if ( ldtValue != null ) {
        dateString = dateUtils.getStringFromLocalDateTime(ldtValue,"MM/dd/yyyy");
    }
    
    assertEquals("03/01/2007", dateString);
}

@Test
public void parseDateReturnsCorrectMMddyyyy() {
    String dateString="03012007";
    LocalDateTime ldtValue = null;
    try {
        ldtValue = dateUtils.parseDate(dateString);
    } catch (Exception e) {
        e.printStackTrace();
        fail(e.getMessage());
    }
    if ( ldtValue != null ) {
        dateString = dateUtils.getStringFromLocalDateTime(ldtValue,"MM/dd/yyyy");
    }
    
    assertEquals("03/01/2007", dateString);
}

@Test
public void parseDateReturnsCorrectMM_dd_yyyy() {
    String dateString="03-01-2007";
    LocalDateTime ldtValue = null;
    try {
        ldtValue = dateUtils.parseDate(dateString);
    } catch (Exception e) {
        e.printStackTrace();
        fail(e.getMessage());
    }
    if ( ldtValue != null ) {
        dateString = dateUtils.getStringFromLocalDateTime(ldtValue,"MM/dd/yyyy");
    }
    
    assertEquals("03/01/2007", dateString);
}

@Test
public void parseDateReturnsCorrectyyyy_MM_dd() {
    String dateString="2007-03-01";
    LocalDateTime ldtValue = null;
    try {
        ldtValue = dateUtils.parseDate(dateString);
    } catch (Exception e) {
        e.printStackTrace();
        fail(e.getMessage());
    }
    if ( ldtValue != null ) {
        dateString = dateUtils.getStringFromLocalDateTime(ldtValue,"MM/dd/yyyy");
    }
    
    assertEquals("03/01/2007", dateString);
}

Тиа адым

2 ответа
2

public LocalDateTime    parseDate(String dateString) {

Ваш выбор форматирования отчасти странен, если есть сомнения, придерживайтесь форматирования IDE по умолчанию (или форматирования проекта.


checkArgument(!Strings.isNullOrEmpty(dateString), "Date [" + dateString + "] is empty of NULL!");

Разделите его на две проверки, одну на ноль и одну на пустоту, что упростит отладку позже.


LOG.debug("parseDate.................................Date [{}]", dateString);

Не заставляйте меня читать в журнале длинную строку, если короткой вполне достаточно:

LOG.debug("MyUtilClass.parseDate("{}")", dateString);

LocalDateTime   ldateResults = null;

Не сокращайте имена переменных только потому, что это возможно, это затрудняет чтение кода и усложняет обслуживание. Речь идет о ltwoXcp, что это вообще должно означать?


dateString = dateString.replace("/","-");

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


try { ldateResults = getLocalDateTimeFromString ( dateString, "MMyyyy" ); }
catch ( Exception ltwoXcp ) {

То же самое и с форматированием. При стандартном форматировании в стиле Java код будет выглядеть так:

try {
    ldateResults = getLocalDateTimeFromString ( dateString, "MMyyyy" );
} catch (Exception ltwoXcp) {

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


LOG.warn("Date [{}] failed with format [MMyyyy], trying another", dateString);

Ведение журнала каждой неудачной попытки синтаксического анализа кажется чрезмерным.


public LocalDateTime getLocalDateTimeFromString(String dateString, String dateFormat) {
    DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(dateFormat);
    if ( dateFormat.length() == 6 ) {
        LocalDate localDate = YearMonth.parse(dateString, dateFormatter).atDay(1);
        return localDate.atStartOfDay();
    }
    return LocalDate.parse(dateString, dateFormatter).atStartOfDay();
}

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


    try {
        ldtValue = dateUtils.parseDate(dateString);
    } catch (Exception e) {
        e.printStackTrace();
        fail(e.getMessage());
    }

Просто объявите throws Exception на методе испытаний и пусть Exception идти своим естественным курсом.


    if ( ldtValue != null ) {
        dateString = dateUtils.getStringFromLocalDateTime(ldtValue,"MM/dd/yyyy");
    }
    
    assertEquals("03/01/2007", dateString);

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


Вы возвращаетесь null, поэтому вызывающий метод должен иметь вывод типа «Не удалось проанализировать <201202302340243> в допустимый LocalDateTime». если ваш парсер возвращает null.


В целом ваша логика слишком сложна, но недостаточно сложна. Вы делаете несколько предположений о том, какие финики вы получите и как с ними обращаться, что может быть справедливым, но может привести к проблемам. Например, неверно сформированная дата все еще может быть проанализирована, «132012» будет проанализировано, даже если это была ошибка ввода пользователя.

Сначала я бы кэшировал средства форматирования:

public static final Map<String, DateTimeFormatter> FORMATTERS;

static {
    Map<String, DateTimeFormatter> formatters = new HashMap<>();
    formatters.put("MMyyyy", new DateTimeFormatter.ofPattern("MMyyyy"));
    formatters.put("yyyyMM", new DateTimeFormatter.ofPattern("yyyyMM"));
    formatters.put("yyyy-MM", new DateTimeFormatter.ofPattern("yyyy-MM"));
    formatters.put("MM-yyyy", new DateTimeFormatter.ofPattern("MM-yyyy"));
    formatters.put("MMddyyyy", new DateTimeFormatter.ofPattern("MMddyyyy"));
    formatters.put("yyyyMMdd", new DateTimeFormatter.ofPattern("yyyyMMdd"));
    formatters.put("MM-dd-yyyy", new DateTimeFormatter.ofPattern("MM-dd-yyyy"));
    formatters.put("yyyy-MM-dd", new DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    FORMATTERS = Collections.unmodifiableMap(formatters);
}

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

Далее, поскольку нам все равно тот много о DateTimeParseException, мы просто проигнорируем это в дополнительном методе:

private LocalDateTime parse(String dateString, String datePattern) {
    DateTimeFormatter formatter = FORMATTERS.get(datePattern);
    
    if (formatter == null) {
        throw new IllegalStateException("No formatter for pattern <" + datePattern + "> has been created.");
    }
    
    TemporalAccessor parsedResult = null;
    
    try {
        parsedResult = formatter.parse(dateString);
    } catch (DateTimeFormatterException ex) {
        // Ignore the exception, we don't care for it.
        return null;
    }
    
    if (!parsedResult.isSupported(ChronoField.DAYE_OF_MONTH)) {
        return YearMonth.from(parsedResult).atDay(1).atStartOfDay();
    } else {
        return LocalDate.of(parsedResult).atStartOfDay();
    }
}

Что мы здесь сделали, так это то, что мы проигнорировали Exception в одной точке. Кроме того, мы отделили присутствие дня месяца в анализируемом значении от длины.

Теперь вернемся к вашей основной логике:

LocalDateTime localDateTime = null;

switch (dateString.length()) {
    case 6:
        localDateTime = parse(dateString, "MMyyyy");
        
        if (localDateTime == null) {
            localDateTime = parse(dateString, "yyyyMM");
        }
        break;
    
    case 7:
        localDateTime = parse(dateString, "yyyy-MM");
        
        if (localDateTime == null) {
            localDateTime = parse(dateString, "MM-yyyy");
        }
        break;
    
    case 8:
        localDateTime = parse(dateString, "MMddyyyy");
        
        if (localDateTime == null) {
            localDateTime = parse(dateString, "yyyyMMdd");
        }
        break;
    
    case 10:
        localDateTime = parse(dateString, "MM-dd-yyyy");
        
        if (localDateTime == null) {
            localDateTime = parse(dateString, "yyyy-MM-dd");
        }
        break;
}

return localDateTime;

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

На первый взгляд кажется, что мы сможем еще больше сократить нашу логику. Мы меняем наши parse способ принять данный DateTimeFormatter и вместо datePattern:

for (Entry<String, DateTimeFormatter> entry : FORMATTERS.entrySet()) {
    String format = entry.getKey();
    DateTimeFormatter formatter = entry.getValue();
    
    if (format.length() == dateString.length()) {
        LocalDateTime result = parse(dateString, formatter);
        
        if (result != null) {
            return result;
        }
    }
}

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

И последнее, но не менее важное: вы можете добавить проверки безопасности в parse чтобы убедиться, что 1100 год не будет неправильно истолкован как месяц, но очень вероятно, что вы никогда не встретите такую ​​дату.

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

private void assertLocalDateTime(int expectedYear, int expectedMonth, LocalDateTime actualLocalDateTime) {
    assertLocalDateTime(expectedYear, expectedMonth, 1, actualLocalDateTime);
}

private void assertLocalDateTime(int expectedYear, int expectedMonth, int expectedDayOfMonth, LocalDateTime actualLocalDateTime) {
    Assertions.assertNotNull(actualLocalDateTime);
    Assertions.assertEquals(expectedYear, actualLocalDateTime.getYear());
    Assertions.assertEquals(expectedMonth, actualLocalDateTime.getMonth());
    Assertions.assertEquals(expectedDayOfMonth, actualLocalDateTime.getDayOfMonth());

    // TODO Assert midnight.
}

При этом ваши модульные тесты будут выглядеть так:

@Test
public void parseDateMonthYear() {
    assertLocalDateTime(2010, 1, Parser.parseDate("012012"));
    assertLocalDateTime(2010, 12, Parser.parseDate("122012"));
}

@Test
public void parseDateYearMonth() {
    assertLocalDateTime(2010, 1, Parser.parseDate("201201"));
    assertLocalDateTime(2010, 12, Parser.parseDate("201212"));
}

И так далее…

  • Спасибо за ввод, попытался использовать TemporalAccessor, как вы предлагали, но он, похоже, не работал в более простых датах, 032007, 200703, 03122007. В любом случае, я все же заставил его работать, как вы обрисовали, с несколькими изменениями вокруг TemporalAccessor … Не уверен , я должен опубликовать окончательное решение?

    — линкольнадым

  • Это не удалось, потому что TemporalAccessor содержит только определенный поля, например, для «032007» он содержит только месяц и год, и из этого вы не можете построить LocalDate. Вот почему в этом случае я пошел в обход YearMonth. И у меня там была опечатка в логике, он конечно использует YearMonth если нет свободного дня.

    — Бобби


  • Обычно мы не публикуем исправленный код, так как он ничего не добавляет к процессу проверки … и мы здесь для проверки (также нам может потребоваться еще раз просмотреть ваше исправленное решение и так далее …).

    — Бобби

«Беспорядок» — это первое, что приходит мне в голову. Почему бы вам просто не использовать цикл по различным форматам?

Если вы хотите ввести особую защиту (например, длину строки), создайте объект синтаксического анализатора, который реализует соответствующую проверку.

Что-то вроде

interface DateParser {
    boolean isApplicable(String in);
    LocalDateTime parse(String in);
}

Затем создайте кучу реализаций и опробуйте их все.

Вы даже можете рассмотреть возможность реализации различных возможностей в перечислении:

enum DateParser {
    LEGACY_1("yyyyMM"),
    LEGACY_2("MMyyyy"),
    ...;
    
    private final int length;
    private final DateTimeFormatter formatter;
    
    private DateParser(String pattern) {
        this.length = pattern.length;
        // note: format created only *once*, not every time
        this.formatter = DateTimeFormatter.ofPattern(pattern);
    }
    
    public boolean isApplicable(String in) {
        return in.length == length;
    }
    
    public LocalDateTime tryParse(String in) {
         ... do whatever you need to do using formatter ...
    }
}

Затем просто переберите константы перечисления и вызовите соответствующие методы.

  • LEGACY_1 Я лучше позвоню им YEAR_MONTH, MONTH_YEAR, DAY_MONTH_YEAR и так далее.

    — Бобби

  • Спасибо за вклад! Кстати, я согласен, это был беспорядок.

    — линкольнадым


  • @Bobby Я думал о такой схеме именования, но в основном она раскрывает детали реализации через имя. На самом деле, я бы предпочел использовать источник формата в именах констант перечисления, таких как MONGO, EXCEL, OLD_MAINFRAME_CRP и так далее.

    — mtj

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

    — Бобби

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *