天天看點

看山聊 Java:檢查日期字元串是否合法

看山聊 Java:檢查日期字元串是否合法

該圖檔由Анастасия Белоусова在Pixabay上釋出

你好,我是看山。

這次說一下,怎樣檢查給出的字元串,是否是合法日期字元串。本文将從 Java 原生和第三方元件兩種方式來說明。

WHY

後端接口在接收資料的時候,都需要進行檢查。檢查全部通過後,才能夠執行業務邏輯。對于時間格式,我們一般需要檢查這麼幾方面:

字元串格式是否正确,比如格式是不是yyyy-MM-dd

時間在合法範圍内,比如我們需要限定在一個月内的時間

字元串可以解析為正常的時間,比如 2 月 30 号就不是正常時間

對于時間格式的判斷,我們可以通過正規表達式來檢查。不過考慮到正規表達式的性能、輸入資料的複雜性,一般能用别的方式,就不選正規表達式。我們還是選擇一種更加通用、更加高效的檢查方式。

首先,定義時間校驗器的接口:

public interface DateValidator {
    boolean isValid(String dateStr);
}
      

接口方法接收一個字元串,傳回布爾類型,表示字元串是否是合法的時間格式。

HOW

接下來就是通過不同方式實作DateValidator。

使用 DateFormat 檢查

Java 提供了格式化和解析時間的工具:DateFormat抽象類和SimpleDataFormat實作類。我們借此實作時間校驗器:

public class DateValidatorUsingDateFormat implements DateValidator {
    private final String dateFormat;

    public DateValidatorUsingDateFormat(String dateFormat) {
        this.dateFormat = dateFormat;
    }

    @Override
    public boolean isValid(String dateStr) {
        final DateFormat sdf = new SimpleDateFormat(this.dateFormat);
        sdf.setLenient(false);
        try {
            sdf.parse(dateStr);
        } catch (ParseException e) {
            return false;
        }
        return true;
    }
}
      

這裡需要注意一下,DateFormat和SimpleDataFormat是非線程安全的,是以每次方法調用時,都需要建立執行個體。

我們通過單元測試驗證下:

class DateValidatorUsingDateFormatTest {

    @Test
    void isValid() {
        final DateValidator validator = new DateValidatorUsingDateFormat("yyyy-MM-dd");

        Assertions.assertTrue(validator.isValid("2021-02-28"));
        Assertions.assertFalse(validator.isValid("2021-02-30"));
    }
}
      

在 Java8 之前,一般都是用這種方式來驗證。Java8 之後,我們有了更多的選擇。

使用 LocalDate 檢查

Java8 引入了更加好用日期和時間 API(想要了解更多内容,請移步參看 Java8 中的時間類及常用 API)。其中包括LocalDate類,是一個不可變且線程安全的時間類。

LocalDate提供了兩個靜态方法,用來解析時間。這兩個方法内部都是使用java.time.format.DateTimeFormatter來處理資料:

// 使用 DateTimeFormatter.ISO_LOCAL_DATE 處理資料
public static LocalDate parse(CharSequence text) {
    return parse(text, DateTimeFormatter.ISO_LOCAL_DATE);
}

// 使用提供的 DateTimeFormatter 處理資料
public static LocalDate parse(CharSequence text, DateTimeFormatter formatter) {
        Objects.requireNonNull(formatter, "formatter");
    return formatter.parse(text, LocalDate::from);
}
      

通過LocalDate的parse方法實作我們的校驗器:

public class DateValidatorUsingLocalDate implements DateValidator {
    private final DateTimeFormatter dateFormatter;

    public DateValidatorUsingLocalDate(DateTimeFormatter dateFormatter) {
        this.dateFormatter = dateFormatter;
    }

    @Override
    public boolean isValid(String dateStr) {
        try {
            LocalDate.parse(dateStr, this.dateFormatter);
        } catch (DateTimeParseException e) {
            return false;
        }
        return true;
    }
}
      

java.time.format.DateTimeFormatter類是不可變的,也就是天然的線程安全,我們可以在不同線程使用同一個校驗器執行個體。

class DateValidatorUsingLocalDateTest {

    @Test
    void isValid() {
        final DateTimeFormatter dateFormatter = DateTimeFormatter.ISO_LOCAL_DATE;
        final DateValidator validator = new DateValidatorUsingLocalDate(dateFormatter);

        Assertions.assertTrue(validator.isValid("2021-02-28"));
        Assertions.assertFalse(validator.isValid("2021-02-30"));
    }
}
      

既然LocalDate#parse是通過DateTimeFormatter實作的,那我們也可以直接使用DateTimeFormatter。

使用 DateTimeFormatter 檢查

DateTimeFormatter解析文本總共分兩步。第一步,根據配置将文本解析為日期和時間字段;第二步,用解析後的字段建立日期和時間對象。

實作驗證器:

public class DateValidatorUsingDateTimeFormatter implements DateValidator {
    private final DateTimeFormatter dateFormatter;

    public DateValidatorUsingDateTimeFormatter(DateTimeFormatter dateFormatter) {
        this.dateFormatter = dateFormatter;
    }

    @Override
    public boolean isValid(String dateStr) {
        try {
            this.dateFormatter.parse(dateStr);
        } catch (DateTimeParseException e) {
            return false;
        }
        return true;
    }
}
      

通過單元測試驗證:

class DateValidatorUsingDateTimeFormatterTest {
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd", Locale.CHINA);

    @Test
    void isValid() {
        final DateTimeFormatter dateFormatter = DATE_FORMATTER.withResolverStyle(ResolverStyle.STRICT);
        final DateValidator validator = new DateValidatorUsingDateTimeFormatter(dateFormatter);

        Assertions.assertTrue(validator.isValid("2021-02-28"));
        Assertions.assertFalse(validator.isValid("2021-02-30"));
    }
}
      

可以看到,我們指定了轉換模式是ResolverStyle.STRICT,這個類型是說明解析模式。共有三種:

STRICT:嚴格模式,日期、時間必須完全正确。

SMART:智能模式,針對日可以自動調整。月的範圍在 1 到 12,日的範圍在 1 到 31。比如輸入是 2 月 30 号,當年 2 月隻有 28 天,傳回的日期就是 2 月 28 日。

LENIENT:寬松模式,主要針對月和日,會自動後延。結果類似于LocalData#plusDays或者LocalDate#plusMonths。

我們通過例子看下差別:

class DateValidatorUsingDateTimeFormatterTest {
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd", Locale.CHINA);

    @Test
    void testResolverStyle() {
        Assertions.assertEquals(LocalDate.of(2021, 2,28), parseDate("2021-02-28", ResolverStyle.STRICT));
        Assertions.assertNull(parseDate("2021-02-29", ResolverStyle.STRICT));

        Assertions.assertEquals(LocalDate.of(2021, 2,28), parseDate("2021-02-28", ResolverStyle.STRICT));
        Assertions.assertNull(parseDate("2021-13-28", ResolverStyle.STRICT));

        Assertions.assertEquals(LocalDate.of(2021, 2,28), parseDate("2021-02-28", ResolverStyle.SMART));
        Assertions.assertEquals(LocalDate.of(2021, 2,28), parseDate("2021-02-29", ResolverStyle.SMART));

        Assertions.assertNull(parseDate("2021-13-28", ResolverStyle.SMART));
        Assertions.assertNull(parseDate("2021-13-29", ResolverStyle.SMART));

        Assertions.assertEquals(LocalDate.of(2021, 2,28), parseDate("2021-02-28", ResolverStyle.LENIENT));
        Assertions.assertEquals(LocalDate.of(2021, 3,1), parseDate("2021-02-29", ResolverStyle.LENIENT));

        Assertions.assertEquals(LocalDate.of(2022, 1,28), parseDate("2021-13-28", ResolverStyle.LENIENT));
        Assertions.assertEquals(LocalDate.of(2022, 2,2), parseDate("2021-13-33", ResolverStyle.LENIENT));
    }

    private static LocalDate parseDate(String dateString, ResolverStyle resolverStyle) {
        try {
            return LocalDate.parse(dateString, DATE_FORMATTER.withResolverStyle(resolverStyle));
        } catch (DateTimeParseException e) {
            return null;
        }
    }
}
      

從例子可以看出,ResolverStyle.STRICT是嚴格控制,用來做時間校驗比較合适;ResolverStyle.LENIENT可以最大程度将字元串轉化為時間對象,在合理範圍内可以随便玩;ResolverStyle.SMART名為智能,但智力有限,兩不沾邊,優勢不夠明顯。JDK 提供的DateTimeFormatter實作,都是ResolverStyle.STRICT模式。

說了 JDK 自帶的實作,接下來說說第三方元件的實作方式。

使用 Apache 出品的 commons-validator 檢查

Apache Commons 項目提供了一個校驗器架構,包含多種校驗規則,包括日期、時間、數字、貨币、IP 位址、郵箱、URL 位址等。本文主要說檢查時間,是以重點看看GenericValidator類提供的isDate方法:

public class GenericValidator implements Serializable {
    // 其他方法

    public static boolean isDate(String value, Locale locale) {
        return DateValidator.getInstance().isValid(value, locale);
    }

    public static boolean isDate(String value, String datePattern, boolean strict) {
        return org.apache.commons.validator.DateValidator.getInstance().isValid(value, datePattern, strict);
    }
}
      

先引入依賴:

<dependency>
    <groupId>commons-validator</groupId>
    <artifactId>commons-validator</artifactId>
    <version>1.7</version>
</dependency>
      
public class DateValidatorUsingCommonsValidator implements DateValidator {
    private final String dateFormat;

    public DateValidatorUsingCommonsValidator(String dateFormat) {
        this.dateFormat = dateFormat;
    }

    @Override
    public boolean isValid(String dateStr) {
        return GenericValidator.isDate(dateStr, dateFormat, true);
    }
}
      
class DateValidatorUsingCommonsValidatorTest {
    @Test
    void isValid() {
        final DateValidator dateValidator = new DateValidatorUsingCommonsValidator("yyyy-MM-dd");

        Assertions.assertTrue(dateValidator.isValid("2021-02-28"));
        Assertions.assertFalse(dateValidator.isValid("2021-02-30"));
    }
}
      

看org.apache.commons.validator.DateValidator#isValid源碼可以發現,内部是通過DateFormat和SimpleDateFormat實作的。

文末總結

在本文中,我們通過四種方式實作了時間字元串校驗邏輯。為了節省篇幅,文中代碼隻提供了核心内容。想要了解具體實作,可以關注公号「看山的小屋」,回複“date”擷取源碼。

推薦閱讀

Java8 中的時間類及常用 API

Date 與 LocalDate 或 LocalDateTime 互相轉換

使用 Java8 中的時間類

檢查日期字元串是否合法