如何在Java中计算“时间之前”?

147
在Ruby on Rails中,有一种功能可以将任何日期打印出多久以前的时间。例如:
8 minutes ago
8 hours ago
8 days ago
8 months ago
8 years ago

有没有在Java中简单的方法来做到这一点?


1
请参考以下链接:https://dev59.com/b3VD5IYBdhLWcg3wXamd 这是C#的代码,但我相信你可以轻松转换它。 - Brandon
33个回答

192
请查看PrettyTime库。
使用方法非常简单:
import org.ocpsoft.prettytime.PrettyTime;

PrettyTime p = new PrettyTime();
System.out.println(p.format(new Date()));
// prints "moments ago"

您还可以传递区域设置以获取国际化信息:

PrettyTime p = new PrettyTime(new Locale("fr"));
System.out.println(p.format(new Date()));
// prints "à l'instant"

正如评论中所述,Android已经将此功能内置到android.text.format.DateUtils类中。


241
如果您正在使用Android系统,可以使用以下方法:android.text.format.DateUtils#getRelativeTimeSpanString()来获取相对时间。 - Somatik
请你能否给你的回答添加更多的描述,仅提供链接的回答现在不太好。 - Ajay S
@Somatik 如果你需要在非Android平台上使用这个功能,你可以在AOSP上查看该类(https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/text/format/DateUtils.java)。 - greg7gkb
@ataylor 这个在安卓中怎么使用? - Hardik Parmar
getRelativeTimeSpanString并不适用于所有情况,这就是为什么我基于这里的许多示例创建了自己的类。请查看我的解决方案:https://dev59.com/Qm865IYBdhLWcg3wWtOz#37042254 - Codeversed
显示剩余3条评论

73

你考虑过使用TimeUnit枚举吗?它对这种情况非常有用。

    try {
        SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy");
        Date past = format.parse("01/10/2010");
        Date now = new Date();

        System.out.println(TimeUnit.MILLISECONDS.toMillis(now.getTime() - past.getTime()) + " milliseconds ago");
        System.out.println(TimeUnit.MILLISECONDS.toMinutes(now.getTime() - past.getTime()) + " minutes ago");
        System.out.println(TimeUnit.MILLISECONDS.toHours(now.getTime() - past.getTime()) + " hours ago");
        System.out.println(TimeUnit.MILLISECONDS.toDays(now.getTime() - past.getTime()) + " days ago");
    }
    catch (Exception j){
        j.printStackTrace();
    }

4
我认为这不是一个完整的答案,因为时间单位是独立的。例如 - 毫秒时间只是分钟时间 * 60 * 1000。你需要从每个时间单位减去下一个更大的时间单位(在将其转换为较低的时间单位后),以便能够在“多长时间前”字符串中使用它。 - Nativ
@Benj - 这个解决方案正确吗?因为一个时间是12小时制,另一个时间是24小时制。请告诉我您对我的问题的反馈。提前感谢您。 - Swift
这是不正确的...每个单元是独立的,就像先前提到的一样。 - Jonathan Laliberte
1
如果你正在开发Android应用,你可以使用以下代码:android.text.format.DateUtils.getRelativeTimeSpanString(milliseconds),它会自动为你处理时间格式。 - Wajid

59

我参考了RealHowTo和Ben J的回答,并自己总结出了我的版本:

public class TimeAgo {
public static final List<Long> times = Arrays.asList(
        TimeUnit.DAYS.toMillis(365),
        TimeUnit.DAYS.toMillis(30),
        TimeUnit.DAYS.toMillis(1),
        TimeUnit.HOURS.toMillis(1),
        TimeUnit.MINUTES.toMillis(1),
        TimeUnit.SECONDS.toMillis(1) );
public static final List<String> timesString = Arrays.asList("year","month","day","hour","minute","second");

public static String toDuration(long duration) {

    StringBuffer res = new StringBuffer();
    for(int i=0;i< TimeAgo.times.size(); i++) {
        Long current = TimeAgo.times.get(i);
        long temp = duration/current;
        if(temp>0) {
            res.append(temp).append(" ").append( TimeAgo.timesString.get(i) ).append(temp != 1 ? "s" : "").append(" ago");
            break;
        }
    }
    if("".equals(res.toString()))
        return "0 seconds ago";
    else
        return res.toString();
}
public static void main(String args[]) {
    System.out.println(toDuration(123));
    System.out.println(toDuration(1230));
    System.out.println(toDuration(12300));
    System.out.println(toDuration(123000));
    System.out.println(toDuration(1230000));
    System.out.println(toDuration(12300000));
    System.out.println(toDuration(123000000));
    System.out.println(toDuration(1230000000));
    System.out.println(toDuration(12300000000L));
    System.out.println(toDuration(123000000000L));
}}

将打印以下内容

0 second ago
1 second ago
12 seconds ago
2 minutes ago
20 minutes ago
3 hours ago
1 day ago
14 days ago
4 months ago
3 years ago

1
这个值得更多的赞。首先,不需要任何库。它仍然干净、优雅且易于更改。 - fangzhzh
3
小建议:使用.append(temp != 1 ? "s" : "")而不是.append(temp > 1 ? "s" : ""),因为0也应该有s后缀。 - berkus
不错的解决方案,如果您想要其他文本,比如“昨天”,非常容易进行修改,或者适应于相对于未来日期的时间段,例如“3周后”。 - user2590928
toDuration方法是否将持续时间参数作为毫秒进行处理?如果是,那么传递1502531374441给我返回的是47年前的时间,但这是我的当前时间。 - Shajeel Afzal
1
@ShajeelAfzal 是的,duration参数是以毫秒为单位的,但它是两个时间之间的差异而不是绝对值。你得到的是从1970年1月1日Unix时间戳开始算起经过的时间。 - Riccardo Casatta
显示剩余7条评论

45
  public class TimeUtils {
    
      public final static long ONE_SECOND = 1000;
      public final static long SECONDS = 60;
    
      public final static long ONE_MINUTE = ONE_SECOND * 60;
      public final static long MINUTES = 60;
      
      public final static long ONE_HOUR = ONE_MINUTE * 60;
      public final static long HOURS = 24;
      
      public final static long ONE_DAY = ONE_HOUR * 24;
    
      private TimeUtils() {
      }
    
      /**
       * converts time (in milliseconds) to human-readable format
       *  "<w> days, <x> hours, <y> minutes and (z) seconds"
       */
      public static String millisToLongDHMS(long duration) {
        StringBuilder res = new StringBuilder();
        long temp = 0;
        if (duration >= ONE_SECOND) {
          temp = duration / ONE_DAY;
          if (temp > 0) {
            duration -= temp * ONE_DAY;
            res.append(temp).append(" day").append(temp > 1 ? "s" : "")
               .append(duration >= ONE_MINUTE ? ", " : "");
          }
    
          temp = duration / ONE_HOUR;
          if (temp > 0) {
            duration -= temp * ONE_HOUR;
            res.append(temp).append(" hour").append(temp > 1 ? "s" : "")
               .append(duration >= ONE_MINUTE ? ", " : "");
          }
    
          temp = duration / ONE_MINUTE;
          if (temp > 0) {
            duration -= temp * ONE_MINUTE;
            res.append(temp).append(" minute").append(temp > 1 ? "s" : "");
          }
    
          if (!res.toString().equals("") && duration >= ONE_SECOND) {
            res.append(" and ");
          }
    
          temp = duration / ONE_SECOND;
          if (temp > 0) {
            res.append(temp).append(" second").append(temp > 1 ? "s" : "");
          }
          return res.toString();
        } else {
          return "0 second";
        }
      }
    
   
      public static void main(String args[]) {
        System.out.println(millisToLongDHMS(123));
        System.out.println(millisToLongDHMS((5 * ONE_SECOND) + 123));
        System.out.println(millisToLongDHMS(ONE_DAY + ONE_HOUR));
        System.out.println(millisToLongDHMS(ONE_DAY + 2 * ONE_SECOND));
        System.out.println(millisToLongDHMS(ONE_DAY + ONE_HOUR + (2 * ONE_MINUTE)));
        System.out.println(millisToLongDHMS((4 * ONE_DAY) + (3 * ONE_HOUR)
            + (2 * ONE_MINUTE) + ONE_SECOND));
        System.out.println(millisToLongDHMS((5 * ONE_DAY) + (4 * ONE_HOUR)
            + ONE_MINUTE + (23 * ONE_SECOND) + 123));
        System.out.println(millisToLongDHMS(42 * ONE_DAY));
        /*
          output :
                0 second
                5 seconds
                1 day, 1 hour
                1 day and 2 seconds
                1 day, 1 hour, 2 minutes
                4 days, 3 hours, 2 minutes and 1 second
                5 days, 4 hours, 1 minute and 23 seconds
                42 days
         */
    }
}

我最终使用了这个修订版。我为您发布了我的修改版本。 - David Blevins
5
David Blevins提供了更多与PrettyTime相关的示例:https://dev59.com/Qm865IYBdhLWcg3wWtOz。因为重复造轮子并且没有推荐第三方库,所以给他一个大负评:-p。 - zakmck

11

这基于RealHowTo的答案,如果您喜欢它,请也关注一下他/她。

这个精简版本允许您指定您可能感兴趣的时间范围。

它还以略微不同的方式处理了“和”部分。我经常发现,在使用分隔符连接字符串时,跳过复杂逻辑并在结束时仅删除最后一个分隔符会更容易。

import java.util.concurrent.TimeUnit;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

public class TimeUtils {

    /**
     * Converts time to a human readable format within the specified range
     *
     * @param duration the time in milliseconds to be converted
     * @param max      the highest time unit of interest
     * @param min      the lowest time unit of interest
     */
    public static String formatMillis(long duration, TimeUnit max, TimeUnit min) {
        StringBuilder res = new StringBuilder();

        TimeUnit current = max;

        while (duration > 0) {
            long temp = current.convert(duration, MILLISECONDS);

            if (temp > 0) {
                duration -= current.toMillis(temp);
                res.append(temp).append(" ").append(current.name().toLowerCase());
                if (temp < 2) res.deleteCharAt(res.length() - 1);
                res.append(", ");
            }

            if (current == min) break;

            current = TimeUnit.values()[current.ordinal() - 1];
        }

        // clean up our formatting....

        // we never got a hit, the time is lower than we care about
        if (res.lastIndexOf(", ") < 0) return "0 " + min.name().toLowerCase();

        // yank trailing  ", "
        res.deleteCharAt(res.length() - 2);

        //  convert last ", " to " and"
        int i = res.lastIndexOf(", ");
        if (i > 0) {
            res.deleteCharAt(i);
            res.insert(i, " and");
        }

        return res.toString();
    }
}

这是一个小代码,让我们试试:

import static java.util.concurrent.TimeUnit.*;

public class Main {

    public static void main(String args[]) {
        long[] durations = new long[]{
            123,
            SECONDS.toMillis(5) + 123,
            DAYS.toMillis(1) + HOURS.toMillis(1),
            DAYS.toMillis(1) + SECONDS.toMillis(2),
            DAYS.toMillis(1) + HOURS.toMillis(1) + MINUTES.toMillis(2),
            DAYS.toMillis(4) + HOURS.toMillis(3) + MINUTES.toMillis(2) + SECONDS.toMillis(1),
            DAYS.toMillis(5) + HOURS.toMillis(4) + MINUTES.toMillis(1) + SECONDS.toMillis(23) + 123,
            DAYS.toMillis(42)
        };

        for (long duration : durations) {
            System.out.println(TimeUtils.formatMillis(duration, DAYS, SECONDS));
        }

        System.out.println("\nAgain in only hours and minutes\n");

        for (long duration : durations) {
            System.out.println(TimeUtils.formatMillis(duration, HOURS, MINUTES));
        }
    }

}

下面将输出以下内容:

0 seconds
5 seconds 
1 day and 1 hour 
1 day and 2 seconds 
1 day, 1 hour and 2 minutes 
4 days, 3 hours, 2 minutes and 1 second 
5 days, 4 hours, 1 minute and 23 seconds 
42 days 

Again in only hours and minutes

0 minutes
0 minutes
25 hours 
24 hours 
25 hours and 2 minutes 
99 hours and 2 minutes 
124 hours and 1 minute 
1008 hours 

如果有人需要,这里有一个类可以把类似上面的任何字符串转换为毫秒。 这对于允许人们以可读文本指定各种超时时间非常有用。


10

有一种简单的方法可以做到这一点:

假设你想要 20 分钟前的时间:

Long minutesAgo = new Long(20);
Date date = new Date();
Date dateIn_X_MinAgo = new Date (date.getTime() - minutesAgo*60*1000);

就这么多了...


2
在大多数情况下,您需要一个“智能”显示,即不是5125分钟前,而是告诉x天前。 - PhiLho

9

关于内置解决方案:

Java没有任何内置的支持格式化相对时间的功能,即使是Java-8和其新的java.time包也不支持。如果你只需要英语而不需要其他语言,那么手工制作的解决方案可能是可以接受的 - 参见@RealHowTo的答案(尽管它有一个强烈的缺点,即不考虑时区将瞬时差转换为本地时间单位!)。无论如何,如果您想避免为其他语言环境编写复杂的自定义解决方案,则需要使用外部库。

在这种情况下,我建议使用我的库Time4J(或Android上的Time4A)。它提供了最大的灵活性和最强大的国际化能力。类net.time4j.PrettyTime有七个方法printRelativeTime...(...)用于此目的。以下是使用测试时钟作为时间源的示例:

TimeSource<?> clock = () -> PlainTimestamp.of(2015, 8, 1, 10, 24, 5).atUTC();
Moment moment = PlainTimestamp.of(2015, 8, 1, 17, 0).atUTC(); // our input
String durationInDays =
  PrettyTime.of(Locale.GERMAN).withReferenceClock(clock).printRelative(
    moment,
    Timezone.of(EUROPE.BERLIN),
    TimeUnit.DAYS); // controlling the precision
System.out.println(durationInDays); // heute (german word for today)

另一个使用 java.time.Instant 作为输入的例子:

String relativeTime = 
  PrettyTime.of(Locale.ENGLISH)
    .printRelativeInStdTimezone(Moment.from(Instant.EPOCH));
System.out.println(relativeTime); // 45 years ago

这个库支持最新版本(v4.17)的80种语言,还支持一些特定于国家/地区的语言环境(尤其是西班牙语、英语、阿拉伯语和法语)。 i18n数据主要基于最新的CLDR版本v29。使用此库的其他重要原因是良好的复数规则支持(这在其他语言环境中通常与英语不同),缩写格式样式(例如:“1秒前”)以及考虑时区的表达方式。 Time4J甚至了解诸如闰秒之类的奇特细节,可以计算相对时间(虽然并不重要,但它形成了与预期视野相关的信息)。由于易于使用的类型转换方法(例如java.time.Instantjava.time.Period),因此与Java-8兼容。

有什么缺点吗?只有两个。

  • 该库不小(也因为其大型i18n数据存储库)。
  • API不太知名,因此社区知识和支持尚不可用,否则提供的文档非常详细和全面。

(紧凑)替代品:

如果您寻找一个更小的解决方案,不需要太多功能,并且愿意容忍与i18n数据相关的可能的质量问题,则:

  • 我建议使用 ocpsoft/PrettyTime(支持实际上 32 种语言(很快 34 种?)适用于仅使用 java.util.Date 的工作 - 请参见 @ataylor 的答案)。行业标准 CLDR(来自 Unicode 联盟)及其庞大的社区背景不幸地不是 i18n-data 的基础,因此进一步的数据增强或改进可能需要一段时间...

  • 如果您在 Android 上,则可以使用辅助类 android.text.format.DateUtils 作为精简内置替代方案(请参见其他评论和答案,在年份和月份方面缺点是没有支持。我确信只有极少数人喜欢这个辅助类的 API 风格。

  • 如果您是 Joda-Time 的粉丝,则可以查看其类 PeriodFormat(在发布 v2.9.4 中支持 14 种语言,另一方面:Joda-Time 当然也不紧凑,所以我在这里提到它只是为了完整性)。这个库不是一个真正的答案,因为根本不支持相对时间。您至少需要添加字面量“ago”(并手动剥离生成的列表格式中的所有较低单元 - 令人尴尬)。与 Time4J 或 Android-DateUtils 不同,它没有特殊的缩写支持或自动从相对时间切换到绝对时间表示。像 PrettyTime 一样,它完全依赖于 Java 社区的私人成员的未经确认的贡献来进行 i18n-data。


8

java.time

Habsq的回答的想法是正确的,但方法错误。

对于与年月日时间轴无关的时间跨度,请使用Period。对于表示24小时时间块而不涉及日历和时分秒的天数,请使用Duration。混合这两个尺度很少有任何意义。

Duration

首先,使用Instant类获取当前在UTC中看到的时刻。

Instant now = Instant.now();  // Capture the current moment as seen in UTC.
Instant then = now.minus( 8L , ChronoUnit.HOURS ).minus( 8L , ChronoUnit.MINUTES ).minus( 8L , ChronoUnit.SECONDS );
Duration d = Duration.between( then , now );

生成小时、分钟和秒的文本。

// Generate text by calling `to…Part` methods.
String output = d.toHoursPart() + " hours ago\n" + d.toMinutesPart() + " minutes ago\n" + d.toSecondsPart() + " seconds ago";

将内容输出到控制台。

System.out.println( "From: " + then + " to: " + now );
System.out.println( output );

从: 2019-06-04T11:53:55.714965Z 到: 2019-06-04T20:02:03.714965Z

8小时前

8分钟前

8秒钟前

周期

首先获取当前日期。

时区是确定日期的关键。对于任何给定的时刻,日期在全球范围内因时区而异。例如,法国巴黎午夜后几分钟就是新的一天,而在魁北克省蒙特利尔仍然是“昨天”。

如果没有指定时区,JVM会隐式地应用其当前默认时区。在运行时,该默认值可能随时更改,因此您的结果可能会有所不同。最好将所需/期望的时区明确指定为参数。如果关键,请与用户确认时区。
大陆/地区的格式指定正确的时区名称,例如America/MontrealAfrica/CasablancaPacific/Auckland。永远不要使用2-4个字母的缩写,如ESTIST,因为它们不是真正的时区,没有标准化,甚至不是唯一的。
ZoneId z = ZoneId.of( "America/Montreal" ) ;  
LocalDate today = LocalDate.now( z ) ;

重新创建一个在八天、月份和年份前的日期。
LocalDate then = today.minusYears( 8 ).minusMonths( 8 ).minusDays( 7 ); // Notice the 7 days, not 8, because of granularity of months. 

计算经过的时间。

Period p = Period.between( then , today );

构建“时间前”字符串的部分。
String output = p.getDays() + " days ago\n" + p.getMonths() + " months ago\n" + p.getYears() + " years ago";

将内容输出到控制台。

System.out.println( "From: " + then + " to: " + today );
System.out.println( output );

从2010年09月27日至2019年06月04日

8天前

8个月前

8年前


关于 java.time

java.time框架内置于Java 8及更高版本中。这些类替代了麻烦的旧遗留日期时间类,如java.util.DateCalendarSimpleDateFormat

要了解更多信息,请参见Oracle教程。并在Stack Overflow上搜索许多示例和解释。规范为JSR 310

Joda-Time项目现已进入维护模式, 建议迁移到java.time类。

您可以直接与数据库交换java.time对象。 使用符合JDBC 4.2或更高版本的JDBC驱动程序即可。不需要字符串,也不需要java.sql.*类。

如何获取java.time类?

ThreeTen-Extra项目通过添加额外的类扩展了java.time。该项目是java.time可能未来添加的潜在试验场。您可能会在这里找到一些有用的类,例如Interval, YearWeek, YearQuarter更多


非常感谢您! - Ticherhaz FreePalestine
这是一个很好的答案 - 而不是添加我的,我会+1并在此提到Duration有一个“Parse”方法,让您更轻松地指定值,例如“P2D”为2天,“P2D2M”为2天和2分钟。这为“2小时前”提供了一个直接的Instance.now().minus(Duraiton.parse("P2H"))。 - Bill K

7

java.time

使用内置于Java 8及其后续版本中的java.time框架。

LocalDateTime t1 = LocalDateTime.of(2015, 1, 1, 0, 0, 0);
LocalDateTime t2 = LocalDateTime.now();
Period period = Period.between(t1.toLocalDate(), t2.toLocalDate());
Duration duration = Duration.between(t1, t2);

System.out.println("First January 2015 is " + period.getYears() + " years ago");
System.out.println("First January 2015 is " + period.getMonths() + " months ago");
System.out.println("First January 2015 is " + period.getDays() + " days ago");
System.out.println("First January 2015 is " + duration.toHours() + " hours ago");
System.out.println("First January 2015 is " + duration.toMinutes() + " minutes ago");

1
那些Duration方法报告了整个持续时间作为总小时数和总分钟数。在Java 8中,该类奇怪地缺乏获取每个小时、分钟和秒的部分的方法。Java 9引入了这些方法,to...Part - Basil Bourque

7

根据这里的许多答案,我为我的用例创建了以下内容。

示例用法:

String relativeDate = String.valueOf(
                TimeUtils.getRelativeTime( 1000L * myTimeInMillis() ));

import java.util.Arrays;
import java.util.List;

import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * Utilities for dealing with dates and times
 */
public class TimeUtils {

    public static final List<Long> times = Arrays.asList(
        DAYS.toMillis(365),
        DAYS.toMillis(30),
        DAYS.toMillis(7),
        DAYS.toMillis(1),
        HOURS.toMillis(1),
        MINUTES.toMillis(1),
        SECONDS.toMillis(1)
    );

    public static final List<String> timesString = Arrays.asList(
        "yr", "mo", "wk", "day", "hr", "min", "sec"
    );

    /**
     * Get relative time ago for date
     *
     * NOTE:
     *  if (duration > WEEK_IN_MILLIS) getRelativeTimeSpanString prints the date.
     *
     * ALT:
     *  return getRelativeTimeSpanString(date, now, SECOND_IN_MILLIS, FORMAT_ABBREV_RELATIVE);
     *
     * @param date String.valueOf(TimeUtils.getRelativeTime(1000L * Date/Time in Millis)
     * @return relative time
     */
    public static CharSequence getRelativeTime(final long date) {
        return toDuration( Math.abs(System.currentTimeMillis() - date) );
    }

    private static String toDuration(long duration) {
        StringBuilder sb = new StringBuilder();
        for(int i=0;i< times.size(); i++) {
            Long current = times.get(i);
            long temp = duration / current;
            if (temp > 0) {
                sb.append(temp)
                  .append(" ")
                  .append(timesString.get(i))
                  .append(temp > 1 ? "s" : "")
                  .append(" ago");
                break;
            }
        }
        return sb.toString().isEmpty() ? "now" : sb.toString();
    }
}

非常有用,非常感谢。 - Haya Akkad

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接