【问题标题】:Multi-locale date parsing多语言环境日期解析
【发布时间】:2016-02-15 12:28:31
【问题描述】:

我正在尝试编写一个类,它能够将多格式和多语言环境的字符串解析为DateTime

multi-format 表示日期可能是:dd/MM/yyyyMMM dd yyyy、...(最多 10 种格式)

multi-locale 表示日期可能是:29 Dec 201529 Dez 2015dice 29 2015 ...(最多 10 个语言环境,例如 engritjp

使用答案Using Joda Date & Time API to parse multiple formats我写道:

val locales = List(
  Locale.ENGLISH,
  Locale.GERMAN,
  ...
)

val patterns = List(
  "yyyy/MM/dd",
  "yyyy-MM-dd",
  "MMMM dd, yyyy",
  "dd MMMM yyyy",
  "dd MMM yyyy"
)

val parsers = patterns.flatMap(patt => locales.map(locale => DateTimeFormat.forPattern(patt).withLocale(locale).getParser)).toArray
val birthDateFormatter = new DateTimeFormatterBuilder().append(null, parsers).toFormatter

但它不起作用:

birthDateFormatter.parseDateTime("29 Dec 2015") // ok
birthDateFormatter.parseDateTime("29 Dez 2015") // exception below

Invalid format: "29 Dez 2015" is malformed at "Dez 2015"
java.lang.IllegalArgumentException: Invalid format: "29 Dez 2015" is
malformed at "Dez 2015"

我发现所有parsers: List[DateTimeParser] 在追加到birthDateFormatter: DateTimeFormatter 后“丢失”了他们的语言环境。而birthDateFormatter 只有一种语言环境——en

我会写:

val birthDateFormatter = locales.map(new DateTimeFormatterBuilder().append(null, parsers).toFormatter.withLocale(_))

并像这样使用它:

birthDateFormatter.map(_.parseDateTime(stringDate))

但它会抛出很多异常。太可怕了。

如何使用 joda-time 解析多格式和多语言环境的字符串? 我该怎么做?

【问题讨论】:

  • 这启发了我实施my own multi-format parser。虽然它不是 Joda-Time,但它可以处理多种格式和语言环境,甚至不会在内部抛出和捕获异常。也许您可以研究源代码,JUnit-test-case 并尝试从中学习。但是,我假设实现不能转移到 Joda-Time 1:1。也许你找到了这样做的方法。另请参阅我的图书馆的tutorial page
  • @MenoHochschild,这是一个很好的解决方案!稍后我将尝试 Time4J。我认为,您应该在主题中添加答案。该解决方案可能对某人有所帮助。
  • 好吧,我将等待答案,直到我设法发布下一个版本的 Time4J (v3.14/v4.11),其中将包含提到的和已经实现的 MultiFormatParser。我希望下周末。我也会尝试找一些时间来更详细地研究性能。
  • 抱歉,回答迟了(见下文),但我想提供 Joda-Time 的替代品,前提是它真的更快并且可以帮助您解决性能问题。

标签: scala jodatime


【解决方案1】:

这很有趣。这是一个帮助我的测试套件(在 Java 中,但我希望你能明白):

import java.util.*;
import java.util.stream.Collectors;

import org.joda.time.DateTime;
import org.joda.time.format.*;
import org.junit.Test;

import static org.assertj.core.api.Assertions.*;

public class JodaTimeLocaleTest {

    @Test // fails on both assertions
    public void testTwoLocales() {
        List<Locale> locales = Arrays.asList(Locale.FRENCH, Locale.GERMAN);
        DateTimeParser[] parsers = locales.stream()
                .map(locale -> DateTimeFormat.forPattern("dd MMM yyyy").withLocale(locale).getParser())
                .collect(Collectors.toList())
                .toArray(new DateTimeParser[0]);
        DateTimeFormatter formatter = new DateTimeFormatterBuilder().append(null, parsers).toFormatter();

        DateTime dateTime1 = formatter.parseDateTime("29 déc. 2015");
        DateTime dateTime2 = formatter.parseDateTime("29 Dez 2015");

        assertThat(dateTime1).isEqualTo(new DateTime("2015-12-29T00:00:00"));
        assertThat(dateTime2).isEqualTo(new DateTime("2015-12-29T00:00:00"));
    }

    @Test // passes
    public void testFrench() {
        DateTimeFormatter formatter = DateTimeFormat.forPattern("dd MMM yyyy").withLocale(Locale.FRENCH);

        DateTime dateTime = formatter.parseDateTime("29 déc. 2015");

        assertThat(dateTime).isEqualTo(new DateTime("2015-12-29T00:00:00"));
    }

    @Test // passes
    public void testGerman() {
        DateTimeFormatter formatter = DateTimeFormat.forPattern("dd MMM yyyy").withLocale(Locale.GERMAN);

        DateTime dateTime = formatter.parseDateTime("29 Dez 2015");

        assertThat(dateTime).isEqualTo(new DateTime("2015-12-29T00:00:00"));
    }
}

首先,你的第一个例子

birthDateFormatter.parseDateTime("29 Dec 2015")

仅因为您的机器的默认语言环境是英语而通过。如果不同的话,这个案子也会失败。这就是为什么我在使用英语语言环境的机器上运行时使用法语和德语的原因。就我而言,这两个断言都失败了。

事实证明,语言环境并未存储在解析器中,而仅存储在格式化程序中。所以当你这样做时

DateTimeFormat.forPattern("dd MMM yyyy").withLocale(locale).getParser()

语言环境是在格式化程序上设置的,但在创建解析器时会丢失:

// DateTimeFormatter#withLocale:
public DateTimeFormatter withLocale(Locale locale) {
    if (locale == getLocale() || (locale != null && locale.equals(getLocale()))) {
        return this;
    }
    // Notice how locale does not affect the parser
    return new DateTimeFormatter(iPrinter, iParser, locale,
            iOffsetParsed, iChrono, iZone, iPivotYear, iDefaultYear);
}

接下来,当你创建一个新的格式化程序时

new DateTimeFormatterBuilder().append(null, parsers).toFormatter()

它是使用系统的默认语言环境创建的(除非您用withLocale() 覆盖它)。并且在解析期间使用该语言环境:

// DateTimeFormatter#parseDateTime
public DateTime parseDateTime(String text) {
    InternalParser parser = requireParser();

    Chronology chrono = selectChronology(null);
    // Notice how the formatter's locale is used
    DateTimeParserBucket bucket = new DateTimeParserBucket(0, chrono, iLocale, iPivotYear, iDefaultYear);
    int newPos = parser.parseInto(bucket, text, 0);
    // ... snipped
}

事实证明,尽管您可以有多个解析器来支持多种格式,但每个格式化程序实例仍然只能使用一个语言环境。

【讨论】:

    【解决方案2】:

    回答问题 1(如何使用 joda-time 解析多格式和多语言环境的字符串?):

    不,这不可能按照您想要的方式进行,另请参阅@Adam Michalik 的好答案。因此,唯一的方法就是编写一个包含多个 Joda 格式化程序的列表,并为给定的输入尝试每一个 - 可能会捕获异常。您已经找到了正确的解决方法,所以我不在这里描述详细信息。

    问题 2 的答案(我怎样才能做到这一点?):

    我的库 Time4J 从 v4.11 开始有了新的MultiFormatParser-class。但是,我发现它的格式引擎通常存在一些性能问题(主要是由于 Java 的自动装箱功能),所以我决定等到 v4.12 版本发布后才得到这个答案,在那里我提高了性能。 根据我的第一个基准测试,Time4J-4.12 似乎比 Joda-Time (v2.9.1) 更快,因为内部异常大大减少了。所以我认为你可以试试最新版本的 Time4J,如果它适合你,然后报告一些反馈。

    private static final MultiFormatParser<PlainDate> TIME4J;
    
    static {
        ChronoFormatter<PlainDate> f1 = 
          ChronoFormatter.ofDatePattern("dd.MM.uuuu", PatternType.CLDR, Locale.ROOT);
        ChronoFormatter<PlainDate> f2 = 
          ChronoFormatter.ofDatePattern("MM/dd/uuuu", PatternType.CLDR, Locale.ROOT);
        ChronoFormatter<PlainDate> f3 = 
          ChronoFormatter.ofDatePattern("uuuu-MM-dd", PatternType.CLDR, Locale.ROOT);
        ChronoFormatter<PlainDate> f4 = 
          ChronoFormatter.ofDatePattern("uuuuMMdd", PatternType.CLDR, Locale.ROOT);
        ChronoFormatter<PlainDate> f5 = 
          ChronoFormatter.ofDatePattern("d. MMMM uuuu", PatternType.CLDR, Locale.GERMAN);
        ChronoFormatter<PlainDate> f6 = 
          ChronoFormatter.ofDatePattern("d. MMMM uuuu", PatternType.CLDR, Locale.FRENCH);
        ChronoFormatter<PlainDate> f7 = 
          ChronoFormatter.ofDatePattern("MMMM d, uuuu", PatternType.CLDR, Locale.US);
        TIME4J = MultiFormatParser.of(f1, f2, f3, f4, f5, f6, f7);
    }
    

    ...

    static List<PlainDate> parse(List<String> input) {
        ParseLog plog = new ParseLog();
        int n = input.size();
        List<PlainDate> result = new ArrayList<>(n);
    
        for (int i = 0; i < n; i++){
            String s = input.get(i);
            plog.reset();
            PlainDate date = TIME4J.parse(s, plog);
            if (!plog.isError()) {
                result.add(date);
            } else {
                // log or report error
            }
        }
        return result;
    }
    
    • MultiFormatParser 中的每个解析器都保留自己的语言环境。
    • 解析器组件的顺序对性能很重要。首选输入中最常见的模式和区域设置。
    • 我强烈建议对 MultiFormatParser 使用静态常量,因为 a) 它是不可变的,b) 在每个库中构建格式化程序的成本都很高(Time4J 也不例外)。
    • 对于与 Joda-Time 的互操作性,您可以考虑以下转换:LocalDate joda = new LocalDate(plainDate.getYear(), plainDate.getMonth(), plainDate.getDayOfMonth()); 但请记住,每次转换都会产生一些额外费用。另一方面,Joda-Time 提供的功能比 Time4J 少,因此后者也可以完成所有与日期-时区相关的任务。
    • 我不是 scala 人,但假设以下 scala 代码可以编译: val parser = MultiFormatParser.of(patterns.flatMap(patt =&gt; locales.map(locale =&gt; ChronoFormatter.ofDatePattern(patt, PatternType.CLDR, locale))).toArray)
    • 顺便说一句:Joda-Time 的性能还不错,因为在 Time4J-v4.12 中让它变得更好对我来说是一项艰巨的任务。解析如此不同的模式和语言环境始终是一项复杂的任务。令我惊讶的是:根据我自己的实验,用 Java-8 构建的新时间库(包java.time)在性能方面是最差的(显然是由于内部异常处理)。
    • 如果您不在 Java-8 平台上工作,那么您可以使用 Time4J-v3.15(向后移植到 Java-6 平台)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-03-15
      • 2010-09-20
      相关资源
      最近更新 更多