preface

Time processing and date format conversion is one of the basic functions of almost all applications, and almost all languages provide basic class libraries for it. As a former heavy user of.NET, thanks to its elegant syntax and, in particular, the divine feature of extensible methods, I have paid little attention to these base libraries. They are like the air you breathe without feeling where they are. Sentiment end, after entering the pit of Java very annoyed with its time processing mode, here to make a summary and memo.

1. Date creates trouble

1.1 Problems with SimpleDateFormat

At the initial stage, I still had absolute trust in the base class library, used Date for the time type without hesitation, and used the SimpleDateFormat class to format dates. Since I would use them frequently in my project, I did something like the following encapsulation:

public class DateUtils { public static SimpleDateFormat DATE_FORMAT; static { DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); } public static String getFormatDate(Date date) { return DATE_FORMAT.format(date); } public static Date parseSimpleDate(String strDate) throws ParseException { return DATE_FORMAT.parse(strDate); }}

After running the unit tests, I applied them as follows:

@Test
public void formatDateTest() throws ParseException {
    Date date = DateUtils.parseSimpleDate("2018-07-12");
    boolean result = DateUtils.getFormatDate(date).equals("2018-07-12");

    Assert.assertTrue(result);
}

But frequent Java on the project after the launch. Lang. A NumberFormatException exceptions, are good fun, check data just know SimpleDateFormat turned out to be thread safe. Look at the source code and locate the problem:

protected Calendar calendar;
// Called from Format after creating a FieldDelegate
private StringBuffer format(Date date, StringBuffer toAppendTo,
                            FieldDelegate delegate) {
    // Convert input date to time field list
    calendar.setTime(date);

    boolean useDateFormatSymbols = useDateFormatSymbols();

    for (int i = 0; i < compiledPattern.length; ) {
        int tag = compiledPattern[i] >>> 8;
        int count = compiledPattern[i++] & 0xff;
        if (count == 255) {
            count = compiledPattern[i++] << 16;
            count |= compiledPattern[i++];
        }

        switch (tag) {
        case TAG_QUOTE_ASCII_CHAR:
            toAppendTo.append((char)count);
            break;

        case TAG_QUOTE_CHARS:
            toAppendTo.append(compiledPattern, i, count);
            i += count;
            break;

        default:
            subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
            break;
        }
    }
    return toAppendTo;
}

The blood tank is empty, so the use of internal variables, back to look at the notes, the others have been friendly hints, ha ha:

/**
 * Date formats are not synchronized.
 * It is recommended to create separate format instances for each thread.
 * If multiple threads access a format concurrently, it must be synchronized
 * externally.
 */

Repeated in the unit test:

1.2 SimpleDateFormat thread unsafe solution

The simplest and most irresponsible way to do this is to lock it:

synchronized (DATE_FORMAT) {
    return DATE_FORMAT.format(strDate);
}

Formatted dates are often used in list data traversal processing, discarded. A better solution is to use it in the thread. Change the code as follows:

public class DateUtils { private static ThreadLocal<DateFormat> THREAD_LOCAL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); public static String getFormatDate(Date date) { return THREAD_LOCAL.get().format(date); } public static Date parseSimpleDate(String strDate) throws ParseException { return THREAD_LOCAL.get().parse(strDate); }}

DATE_FORMAT is defined as public and is used as a static variable in some special situations. It can’t be solved by just changing a utility class. It’s a problem. So simply abandon SimpleDateFormat in mons. Org.apache.com lang3. Time. DateFormatUtils instead, change the code is as follows:

public class DateUtils extends org.apache.commons.lang3.time.DateUtils { public static String DATE_PATTERN = "yyyy-MM-dd"; public static String getFormatDate(Date date) { return DateFormatUtils.format(date, DATE_PATTERN); } public static Date parseSimpleDate(String strDate) throws ParseException { return parseDate(strDate, DATE_PATTERN); }}

1.3 Annoying Calendar

In addition to date format conversion, application of another big demand for processing time is calculated, grateful to org.apache.com mons. Lang3. Time. DateUtils this tools for us to do most of the packaging, can think of some basic calculation can be directly used “no brains”. But sometimes you still have to pass in all sorts of Calendar arguments, especially those pesky bunch of constants:

public final static int ERA = 0;
public final static int YEAR = 1;
public final static int MONTH = 2;
public final static int WEEK_OF_YEAR = 3;
public final static int WEEK_OF_MONTH = 4;
public final static int DATE = 5;
public final static int DAY_OF_MONTH = 5;
public final static int DAY_OF_YEAR = 6;
public final static int DAY_OF_WEEK = 7;
public final static int DAY_OF_WEEK_IN_MONTH = 8;
public final static int AM_PM = 9;
public final static int HOUR = 10;
public final static int HOUR_OF_DAY = 11;
public final static int MINUTE = 12;
public final static int SECOND = 13;
public final static int MILLISECOND = 14;
public final static int ZONE_OFFSET = 15;
public final static int DST_OFFSET = 16;
public final static int FIELD_COUNT = 17;
public final static int SUNDAY = 1;
public final static int MONDAY = 2;
public final static int TUESDAY = 3;
public final static int WEDNESDAY = 4;
public final static int THURSDAY = 5;
public final static int FRIDAY = 6;
public final static int SATURDAY = 7;
public final static int JANUARY = 0;
public final static int FEBRUARY = 1;
public final static int MARCH = 2;
public final static int APRIL = 3;
public final static int MAY = 4;
public final static int JUNE = 5;
public final static int JULY = 6;
public final static int AUGUST = 7;
public final static int SEPTEMBER = 8;
public final static int OCTOBER = 9;
public final static int NOVEMBER = 10;
public final static int DECEMBER = 11;
public final static int UNDECIMBER = 12;
public final static int AM = 0;
public final static int PM = 1;
public static final int ALL_STYLES = 0;

Possible existence is reasonable, I certainly am not qualified to judge anything, I just don’t like. Of course, there must be a large and comprehensive method, but it must be different from the commonly used scheme. Maybe there will be a voice saying, “You can pull it out by yourself.” Yes, e.g.

public static Date getBeginOfMonth(Date date) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(date);

    calendar.set(Calendar.DATE, 1);
    calendar.set(Calendar.HOUR_OF_DAY, 0);
    calendar.set(Calendar.MINUTE, 0);
    calendar.set(Calendar.SECOND, 0);
    calendar.set(Calendar.MILLISECOND, 0);

    return calendar.getTime();
}

public static Date getEndOfMonth(Date date) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(date);

    calendar.set(Calendar.DATE, calendar.getActualMaximum(Calendar.DATE));
    calendar.set(Calendar.HOUR_OF_DAY, calendar.getActualMaximum(Calendar.HOUR_OF_DAY));
    calendar.set(Calendar.MINUTE, calendar.getActualMaximum(Calendar.MINUTE));
    calendar.set(Calendar.SECOND, calendar.getActualMaximum(Calendar.SECOND));
    calendar.set(Calendar.MILLISECOND, calendar.getActualMaximum(Calendar.MILLISECOND));

    return calendar.getTime();
}

Even though this code will only exist in the utility class, just hate it, they shouldn’t have been written out of my hand.

2. Instant Redemption

The new date core classes in Java8 are as follows:

Instant
LocalDate
LocalTime
LocalDateTime

There are a few other timezones and computation-related classes that will be covered in the following code examples. Instant is the main object here, but if you look at the source code, you can see that it contains only two key fields:

/** * The number of seconds from the epoch of 1970-01-01T00:00:00Z. */ private final long seconds; /** * The number of nanoseconds, later along the time-line, from the seconds field. * This is always positive, And never exceed 999,999,999. */ private final int nanos; /** * Gets the number of seconds from the Java epoch of 1970-01-01T00:00:00Z. * <p> * The epoch second count is a simple  incrementing count of seconds where * second 0 is 1970-01-01T00:00:00Z. * The nanosecond part of the day is returned by  {@code getNanosOfSecond}. * * @return the seconds from the epoch of 1970-01-01T00:00:00Z */ public long getEpochSecond() { return seconds; } /** * Gets the number of nanoseconds, later along the time-line, from the start * of the second. * <p> * The nanosecond-of-second value measures the total number of nanoseconds from * the second returned by {@code getEpochSecond}. * * @return the nanoseconds within the second, always positive, Never exceed 999,999,999 */ public int getNano() {return nanos; }

The combination of seconds and nanoseconds is pretty much the best way to deal with time. The absolute time is the same all over the world, so it’s a good idea to set aside the annoying time zones and the melodramatic daylight saving time.

2.1 Intertransfer between Instant and LocalDateTime

Since Instant does not contain time zone information, you need to specify the time zone when converting. Let’s take a look at the following example:

@Test
public void timeZoneTest() {
    Instant instant = Instant.now();

    LocalDateTime timeForChina = LocalDateTime.ofInstant(instant, ZoneId.of("Asia/Shanghai"));
    LocalDateTime timeForAmerica = LocalDateTime.ofInstant(instant, ZoneId.of("America/New_York"));
    long dif = Duration.between(timeForAmerica, timeForChina).getSeconds() / 3600;

    Assert.assertEquals(dif, 12L);
}

The geographical time difference should be 13 hours, but the United States uses daylight saving time, so the actual time difference is 12 hours. The above unit test can prove that LocalDateTime has helped us deal with the problem of daylight saving time. The source code is as follows:

public static LocalDateTime ofInstant(Instant instant, ZoneId zone) { Objects.requireNonNull(instant, "instant"); Objects.requireNonNull(zone, "zone"); ZoneRules rules = zone.getRules(); ZoneOffset offset = rules.getOffset(instant); return ofEpochSecond(instant.getEpochSecond(), instant.getNano(), offset); }...

The key class to get the time offset is Zonerules, and the reverse conversion is also very simple, refer to the source code:

@Test
public void instantTest() {

    LocalDateTime time = LocalDateTime.parse("2018-07-13T00:00:00");
    ZoneRules rules = ZoneId.of("Asia/Shanghai").getRules();
    Instant instant = time.toInstant(rules.getOffset(time));

    LocalDateTime timeBack = LocalDateTime.ofInstant(instant, ZoneId.of("Asia/Shanghai"));

    Assert.assertEquals(time, timeBack);
}

2.2 Let’s talk about formatting

The new dateTimeFormatter class is not quite as freeform as C#, but it is thread-safe to use. It also has several common format templates preset to make it easier to encapsulate them further.

The common way to convert a date from a string is as follows:

@Test public void parse() { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); Parse ("2018-07-13 12:05:30.505",formatter); System.out.println(time); }

There are three things that make the above example code very annoying. First, I need to get a timetype, which has no formatting issues, so it looks silly to explicitly specify a template and then convert it. Second, the display format of the time is regular and easily exhaustible, there are just a few, but also need to be explicitly passed into the template, looks silly; Third, localdatetime.parse () does not support a template in the localDate format and looks silly.

So I made a simple encapsulation of it, as follows:

public class DateUtils { public static HashMap<String, String> patternMap; static { patternMap = new HashMap<>(); // July 13, 2018 12:5:30, 2018-07-13 12:05:30, 2018/07/13 12:05:30 patternMap.put("^\\d{4}\\D+\\d{2}\\D+\\d{2}\\D+\\d{2}\\D+\\d{2}\\D+\\d{2}\\D+\\d{3}\\D*$", "yyyy-MM-dd-HH-mm-ss-SSS"); patternMap.put("^\\d{4}\\D+\\d{2}\\D+\\d{2}\\D+\\d{2}\\D+\\d{2}\\D+\\d{2}\\D*$", "yyyy-MM-dd-HH-mm-ss"); patternMap.put("^\\d{4}\\D+\\d{2}\\D+\\d{2}\\D*$", "yyyy-MM-dd"); // 20180713120530 patternMap.put("^\\d{14}$", "yyyyMMddHHmmss"); patternMap.put("^\\d{8}$", "yyyyMMdd"); } public static LocalDateTime parse(String text) { for (String key : patternMap.keySet()) { if (Pattern.compile(key).matcher(text).matches()) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern(patternMap.get(key)); text = text.replaceAll("\\D+", "-") .replaceAll("-$", ""); return parse(formatter, text); } } throw new DateTimeException("can't match a suitable pattern!" ); } public static LocalDateTime parse(DateTimeFormatter formatter, String text) { TemporalAccessor accessor = formatter.parseBest(text, LocalDateTime::from, LocalDate::from); LocalDateTime time; if (accessor instanceof LocalDate) { LocalDate date = LocalDate.from(accessor); time = LocalDateTime.of(date, LocalTime.MIDNIGHT); } else { time = LocalDateTime.from(accessor); } return time; }}

Testing:

@test public void parse() {String[] Array = new String[]{"2018-07-13 12:05:30", "2018/07/13 12:05:30.505", @test public void parse() {String[] Array = new String[]; "Jul. 13, 2018 12:05:30 "," Jul. 13, 2018 12:05:30 505 milliseconds ", "2018-07-13", "20180713", "20180713120530",}; System.out.println("-------------------------"); for (String s : array) { System.out.println(DateUtils.parse(s)); } System.out.println("-------------------------"); }

The above examples should suffice for most application scenarios. Add them to the PatternMap if special situations arise.

On the other hand, the date is turned into a string. At this time, passing in a pattern is said to be past, because the display format becomes the core business for this scenario. Example:

@Test public void format() { LocalDateTime time = LocalDateTime.of(2018, 7, 13, 12, 5, 30); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); Assert. AssertEquals (time. The format (the formatter), "the 2018-07-13 12:05:30. 000"); }

Of course, common formats should also be encased in tool classes.


My official account “Jieyi”