Аналог функции ORACLE MONTHS_BETWEEN в Java - PullRequest
7 голосов
/ 01 февраля 2012

Есть ли у Java аналог функции Oracle MONTHS_BETWEEN?

Ответы [ 8 ]

3 голосов
/ 01 апреля 2015

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

Рассмотрим месяцы с 17.02.2013 по 03.03.2016 ("dd/MM/yyyy")Результат Oracle: 36,8064516129032Метод Java от @ Alain.janinm ответ: 36.74193548387097

Вот изменения, которые я сделал, чтобы получить более близкий результат к функции Oracle months_between():

public static double monthsBetween(Date startDate, Date endDate){  

    Calendar cal = Calendar.getInstance(); 

    cal.setTime(startDate);  
    int startDayOfMonth = cal.get(Calendar.DAY_OF_MONTH);
    int startMonth =  cal.get(Calendar.MONTH);
    int startYear = cal.get(Calendar.YEAR);  

    cal.setTime(endDate);
    int endDayOfMonth = cal.get(Calendar.DAY_OF_MONTH);  
    int endMonth =  cal.get(Calendar.MONTH);
    int endYear = cal.get(Calendar.YEAR);  


    int diffMonths = endMonth - startMonth;
    int diffYears = endYear - startYear;
    int diffDays = endDayOfMonth - startDayOfMonth;

    return (diffYears * 12) + diffMonths + diffDays/31.0;
} 

С помощью этой функции результат вызова для дат 17/02/2013 и 11/03/2016: 36.806451612903224

Примечание. Из моего понимания Oracle months_between() функция считает, что все месяцы имеют длину 31 день

2 голосов
/ 12 июня 2018

Мне пришлось перенести некоторый код Oracle в java, и я не нашел аналога в функции month_between oracle. При тестировании перечисленных примеров обнаружены случаи, когда они дают неверные результаты.

Итак, создал свою собственную функцию. Создано 1600+ тестов, сравнивающих результаты db против моей функции, включая дату и компонент времени - все работает нормально.

Надеюсь, это может кому-то помочь.

public static double oracle_months_between(Timestamp endDate,Timestamp startDate) {

    //MONTHS_BETWEEN returns number of months between dates date1 and date2.
    // If date1 is later than date2, then the result is positive.
    // If date1 is earlier than date2, then the result is negative.
    // If date1 and date2 are either the same days of the month or both last days of months, then the result is always an integer.
    // Otherwise Oracle Database calculates the fractional portion of the result based on a 31-day month and considers the difference in time components date1 and date2.

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String endDateString = sdf.format(endDate), startDateString = sdf.format(startDate);

    int startDateYear = Integer.parseInt(startDateString.substring(0,4)), startDateMonth = Integer.parseInt(startDateString.substring(5,7)), startDateDay = Integer.parseInt(startDateString.substring(8,10));
    int endDateYear = Integer.parseInt(endDateString.substring(0,4)), endDateMonth = Integer.parseInt(endDateString.substring(5,7)), endDateDay = Integer.parseInt(endDateString.substring(8,10));

    boolean endDateLDM = is_last_day(endDate), startDateLDM = is_last_day(startDate);

    int diffMonths = -startDateYear*12 - startDateMonth + endDateYear * 12 + endDateMonth;

    if (endDateLDM && startDateLDM || extract_day(startDate) == extract_day(endDate)){
        // If date1 and date2 are either the same days of the month or both last days of months, then the result is always an integer.
        return (double)(diffMonths);
    }

    double diffDays = (endDateDay - startDateDay)/31.;

    Timestamp dStart = Timestamp.valueOf("1970-01-01 " + startDateString.substring(11)), dEnd = Timestamp.valueOf("1970-01-01 " + endDateString.substring(11));

    return diffMonths + diffDays + (dEnd.getTime()-dStart.getTime())/1000./3600./24./31.;
}

public static boolean is_last_day(Timestamp ts){
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(ts);
    int max = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
    return max == Integer.parseInt((new SimpleDateFormat("dd").format(ts)));
}
2 голосов
/ 01 февраля 2012

Вы можете сделать это с:

public static int monthsBetween(Date minuend, Date subtrahend){  

    Calendar cal = Calendar.getInstance();   
    cal.setTime(minuend);  
    int minuendMonth =  cal.get(Calendar.MONTH);  
    int minuendYear = cal.get(Calendar.YEAR);  
    cal.setTime(subtrahend);  
    int subtrahendMonth =  cal.get(Calendar.MONTH);  
    int subtrahendYear = cal.get(Calendar.YEAR);  

    return ((minuendYear - subtrahendYear) * (cal.getMaximum(Calendar.MONTH)+1)) +    
    (minuendMonth - subtrahendMonth);  
}  

Редактировать:

Согласно этой документации MONTHS_BETWEEN возвращает дробный результат, я думаю, этот метод делает то же самое:

public static void main(String[] args) throws ParseException {
    SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
    Date d = sdf.parse("02/02/1995");
    Date d2 = sdf.parse("01/01/1995");
    System.out.println(monthsBetween(d, d2));

}

public static double monthsBetween(Date baseDate, Date dateToSubstract){  

    Calendar cal = Calendar.getInstance();   
    cal.setTime(baseDate);
    int baseDayOfYear = cal.get(Calendar.DAY_OF_YEAR);  
    int baseMonth =  cal.get(Calendar.MONTH);  
    int baseYear = cal.get(Calendar.YEAR);  

    cal.setTime(dateToSubstract);  
    int subDayOfYear = cal.get(Calendar.DAY_OF_YEAR);
    int subMonth =  cal.get(Calendar.MONTH);  
    int subYear = cal.get(Calendar.YEAR);  

    //int fullMonth = ((baseYear - subYear) * (cal.getMaximum(Calendar.MONTH)+1)) +    
    //(baseMonth - subMonth);  
    //System.out.println(fullMonth);

    return ((baseYear - subYear) * (cal.getMaximum(Calendar.MONTH)+1)) +   
           (baseDayOfYear-subDayOfYear)/31.0;
} 
0 голосов
/ 01 февраля 2018

java.time

В других ответах используется проблемный старый класс Calendar, который теперь унаследован и заменен классами java.time .

MONTHS_BETWEEN

Документ говорит :

MONTHS_BETWEEN возвращает количество месяцев между датами date1 и date2. Если дата1 позже даты2, то результат положительный. Если date1 раньше, чем date2, то результат отрицательный. Если date1 и date2 являются либо одними и теми же днями месяца, либо обоими последними днями месяцев, то результатом всегда будет целое число. В противном случае Oracle Database рассчитывает дробную часть результата на основе 31-дневного месяца и учитывает разницу во временных компонентах date1 и date2.

LocalDate

Класс LocalDate представляет значение только для даты без времени суток и без часового пояса.

Получить LocalDate из базы данных, используя JDBC 4.2 и более поздние версии. Класс java.sql.Date теперь унаследован и его можно избежать.

LocalDate start = myResultSet.getObject( … , LocalDate.class ) ;  // Retrieve a `LocalDate` from database using JDBC 4.2 and later.

Для нашей демонстрации давайте смоделируем эти найденные даты.

LocalDate start = LocalDate.of( 2018 , Month.JANUARY , 23 );
LocalDate stop = start.plusDays( 101 );

Period

Рассчитать прошедшее время как промежуток времени, не привязанный к временной шкале, a Period.

Period p = Period.between( start , stop );

Извлечь общее количество месяцев .

long months = p.toTotalMonths() ; 

Извлеките количество дней, составляющих , дни, оставшиеся после расчета месяцев.

int days = p.getDays() ;

BigDecimal

Для точности используйте BigDecimal. В типах double и Double используется технология с плавающей запятой , снижающая точность для быстрого выполнения.

Преобразовать наши значения из примитивов в BigDecimal.

BigDecimal bdDays = new BigDecimal( days );
BigDecimal bdMaximumDaysInMonth = new BigDecimal( 31 );

Разделите, чтобы получить наш дробный месяц. MathContext предоставляет ограничение для разрешения дробного числа, а также режим округления, чтобы туда попасть. Здесь мы используем константу MathContext.DECIMAL32, потому что я предполагаю, что функция Oracle использует 32-битную математику. Режим округления - RoundingMode.HALF_EVEN, по умолчанию задан IEEE 754 , а также известен как «Банковское округление» , что математически более справедливо, чем «округление в школе» ”Обычно учат детей.

BigDecimal fractionalMonth = bdDays.divide( bdMaximumDaysInMonth , MathContext.DECIMAL32 ); 

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

BigDecimal bd = new BigDecimal( months ).add( fractionalMonth );

Чтобы более близко имитировать поведение функции Oracle, вы можете преобразовать в double.

double d = bd.round( MathContext.DECIMAL32 ).doubleValue();

Oracle не документировал кровавые подробности их расчета. Поэтому вам может потребоваться провести эксперименты с пробами и ошибками, чтобы проверить, соответствует ли этот код вашим функциям Oracle.

Дамп на консоль.

System.out.println( "From: " + start + " to: " + stop + " = " + bd + " months, using BigDecimal. As a double: " + d );

См. Этот код, запущенный на IdeOne.com .

С: 2018-01-23 до: 2018-05-04 = 3,3548387 месяцев с использованием BigDecimal. Как двойной: 3.354839

Предостережение: Пока я отвечал на Вопрос в соответствии с просьбой, я должен отметить: Отслеживание прошедшего времени в виде дроби, как видно здесь, неразумно . Вместо этого используйте классы java.time Period и Duration. Для текстового представления используйте стандартный формат ISO 8601 : PnYnMnDTnHnMnS. Например, Period видно в нашем примере выше: P3M11D в течение трех месяцев и одиннадцати дней.


О java.time

Фреймворк java.time встроен в Java 8 и более поздние версии. Эти классы вытесняют проблемные старые устаревшие классы даты и времени, такие как java.util.Date, Calendar, & SimpleDateFormat.

Проект Joda-Time , теперь в режиме обслуживания , рекомендует выполнить переход на классы java.time .

Чтобы узнать больше, см. Oracle Tutorial . И поиск переполнения стека для многих примеров и объяснений. Спецификация JSR 310 .

Где получить классы java.time?

  • Java SE 8 , Java SE 9 и выше
    • Встроенный.
    • Часть оf стандартный Java API с включенной реализацией.
    • Java 9 добавляет некоторые незначительные функции и исправления.
  • Java SE 6 и Java SE 7
    • Большая часть функций java.time перенесена на Java 6 & 7 в ThreeTen-Backport .
  • Android
    • Более поздние версии реализации комплекта Androidклассов java.time.
    • Для более ранних версий Android проект ThreeTenABP адаптируется ThreeTen-Backport (упоминаетсявыше).См. Как использовать ThreeTenABP… .

ThreeTen-Extra Проект расширяет java.time дополнительными классами.Этот проект является полигоном для возможных будущих дополнений к java.time.Здесь вы можете найти несколько полезных классов, таких как Interval, YearWeek, YearQuarter и more .

0 голосов
/ 01 февраля 2018

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

public static BigDecimal monthsBetween(final Date start, final Date end, final ZoneId zone, final int scale ) {
    final BigDecimal no31 = new BigDecimal(31);

    final LocalDate ldStart = start.toInstant().atZone(zone).toLocalDate();
    final LocalDate ldEnd = end.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

    final int endDay = ldEnd.getDayOfMonth();
    final int endMonth = ldEnd.getMonthValue();
    final int endYear = ldEnd.getYear();
    final int lastDayOfEndMonth = ldEnd.lengthOfMonth();

    final int startDay = ldStart.getDayOfMonth();
    final int startMonth = ldStart.getMonthValue();
    final int startYear = ldStart.getYear();
    final int lastDayOfStartMonth = ldStart.lengthOfMonth();

    final BigDecimal diffInMonths = new BigDecimal((endYear - startYear)*12+(endMonth-startMonth));
    final BigDecimal fraction;
    if(endDay==startDay || (endDay==lastDayOfEndMonth && startDay==lastDayOfStartMonth)) {
        fraction = BigDecimal.ZERO;
    }
    else {
        fraction = BigDecimal.valueOf(endDay-startDay).divide(no31, scale, BigDecimal.ROUND_HALF_UP);
    }
    return diffInMonths.add(fraction);
}

public static BigDecimal monthsBetween(final Date start, final Date end) {
    return monthsBetween(start, end, ZoneId.systemDefault(), 20);
}
0 голосов
/ 08 июня 2016

У меня та же проблема, и после Oracle MONTHS_BETWEEN я внес некоторые изменения в ответы @ alain.janinm и @ Guerneen4, чтобы исправить некоторые случаи:

Рассмотрим месяцы между 31/07/1998 и30/09/2013 ("дд / мм / гггг") Результат Oracle: 182 Java-метод от @ Guerneen4 ответ: 181.96774193548387

Проблема заключается в том, что согласно спецификации, если date1 и date2 оба последних дня месяца,тогда результат всегда будет целым числом.

Для простоты понимания здесь вы можете найти спецификации Oracle MONTHS_BETWEEN: https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions089.htm. Я копирую здесь, чтобы подвести итог:

"возвращает количество месяцев между датамиdate1 и date2. Если date1 позже, чем date2, то результат положительный. Если date1 раньше, чем date2, то результат отрицательный. Если date1 и date2 являются либо одинаковыми днями месяца, либо обоими последними днями месяцев, торезультат всегда является целым числом, в противном случае Oracle Database рассчитывает дробную часть результата на основе 31-дневного значения.и рассмотрим разницу во временных компонентах date1 и date2. "

Вот изменения, которые я сделал, которые дают наиболее близкий результат функции Oracle months_between ():

public static double monthsBetween(Date startDate, Date endDate) {
    Calendar calSD = Calendar.getInstance();
    Calendar calED = Calendar.getInstance();

    calSD.setTime(startDate);
    int startDayOfMonth = calSD.get(Calendar.DAY_OF_MONTH);
    int startMonth = calSD.get(Calendar.MONTH);
    int startYear = calSD.get(Calendar.YEAR);

    calED.setTime(endDate);
    int endDayOfMonth = calED.get(Calendar.DAY_OF_MONTH);
    int endMonth = calED.get(Calendar.MONTH);
    int endYear = calED.get(Calendar.YEAR);

    int diffMonths = endMonth - startMonth;
    int diffYears = endYear - startYear;
    int diffDays = calSD.getActualMaximum(Calendar.DAY_OF_MONTH) == startDayOfMonth
            && calED.getActualMaximum(Calendar.DAY_OF_MONTH) == endDayOfMonth ? 0 : endDayOfMonth - startDayOfMonth;

    return (diffYears * 12) + diffMonths + diffDays / 31.0;
}
0 голосов
/ 28 декабря 2012

Предыдущие ответы не идеальны, потому что они не обрабатывают даты, такие как 31 февраля.

Вот моя итерационная интерпретация MONTHS_BETWEEN в Javascript ...

    // Replica of the Oracle function MONTHS_BETWEEN where it calculates based on 31-day months
    var MONTHS_BETWEEN = function(d1, d2) {
        // Don't even try to calculate if it's the same day
        if (d1.getTicks() === d2.getTicks()) return 0;

        var totalDays = 0;
        var earlyDte = (d1 < d2 ? d1 : d2); // Put the earlier date in here
        var laterDate = (d1 > d2 ? d1 : d2); // Put the later date in here
        // We'll need to compare dates using string manipulation because dates such as 
        // February 31 will not parse correctly with the native date object
        var earlyDteStr = [(earlyDte.getMonth() + 1), earlyDte.getDate(), earlyDte.getFullYear()];

        // Go in day-by-day increments, treating every month as having 31 days
        while (earlyDteStr[2] < laterDate.getFullYear() ||
               earlyDteStr[2] == laterDate.getFullYear() && earlyDteStr[0] < (laterDate.getMonth() + 1) ||
               earlyDteStr[2] == laterDate.getFullYear() && earlyDteStr[0] == (laterDate.getMonth() + 1) && earlyDteStr[1] < laterDate.getDate()) {
            if (earlyDteStr[1] + 1 < 32) {
                earlyDteStr[1] += 1; // Increment the day
            } else {
                // If we got to this clause, then we need to carry over a month
                if (earlyDteStr[0] + 1 < 13) {
                    earlyDteStr[0] += 1; // Increment the month
                } else {
                    // If we got to this clause, then we need to carry over a year
                    earlyDteStr[2] += 1; // Increment the year
                    earlyDteStr[0] = 1; // Reset the month
                }
                earlyDteStr[1] = 1; // Reset the day
            }

            totalDays += 1; // Add to our running sum of days for this iteration
        }
        return (totalDays / 31.0);
    };
0 голосов
/ 02 февраля 2012

В Время Joda Между классами org.joda.time.Months существует месяцы.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...