From 349fc0188741184aa7c270b921c1b8aa78c341a1 Mon Sep 17 00:00:00 2001 From: Swetank Mohanty Date: Mon, 17 Jun 2024 23:34:38 +0530 Subject: [PATCH] New utility methods for Date and removed workflows (#9) * Added new utility methods in DateUtils * Added new utility methods for DateUtils * Deleted CodeQL and Maven Publish workflows --- .github/workflows/codeql.yml | 91 ----------- .github/workflows/maven-publish.yml | 34 ---- .../primekit/essentials/common/YearWeek.java | 32 ++++ .../essentials/common/util/DateUtils.java | 152 +++++++++++++++++- .../primekit/essentials/DateUtilsTest.java | 30 +++- 5 files changed, 209 insertions(+), 130 deletions(-) delete mode 100644 .github/workflows/codeql.yml delete mode 100644 .github/workflows/maven-publish.yml create mode 100644 primekit-essentials/src/main/java/com/shortthirdman/primekit/essentials/common/YearWeek.java diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 4c5d135..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,91 +0,0 @@ -# # For most projects, this workflow file will not need changing; you simply need -# # to commit it to your repository. -# # -# # You may wish to alter this file to override the set of languages analyzed, -# # or to provide custom queries or build logic. -# # -# # ******** NOTE ******** -# # We have attempted to detect the languages in your repository. Please check -# # the `language` matrix defined below to confirm you have the correct set of -# # supported CodeQL languages. -# # -# # name: "CodeQL" - -# # on: - # # push: - # # branches: [ "main" ] - # # pull_request: - # # branches: [ "main" ] - # # schedule: - # # - cron: '26 2 * * 6' - -# # jobs: - # # analyze: - # # name: Analyze (${{ matrix.language }}) - # # Runner size impacts CodeQL analysis time. To learn more, please see: - # # - https://gh.io/recommended-hardware-resources-for-running-codeql - # # - https://gh.io/supported-runners-and-hardware-resources - # # - https://gh.io/using-larger-runners (GitHub.com only) - # # Consider using larger runners or machines with greater resources for possible analysis time improvements. - # runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - # timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} - # permissions: - # # required for all workflows - # security-events: write - - # # required to fetch internal or private CodeQL packs - # packages: read - - # # only required for workflows in private repositories - # actions: read - # contents: read - - # strategy: - # fail-fast: false - # matrix: - # include: - # # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' - # # Use `c-cpp` to analyze code written in C, C++ or both - # # Use 'java-kotlin' to analyze code written in Java, Kotlin or both - # # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both - # # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, - # # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. - # # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how - # # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages - # steps: - # - name: Checkout repository - # uses: actions/checkout@v4 - - # # Initializes the CodeQL tools for scanning. - # - name: Initialize CodeQL - # uses: github/codeql-action/init@v3 - # with: - # languages: ${{ matrix.language }} - # build-mode: ${{ matrix.build-mode }} - # # If you wish to specify custom queries, you can do so here or in a config file. - # # By default, queries listed here will override any specified in a config file. - # # Prefix the list here with "+" to use these queries and those in the config file. - - # # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # # queries: security-extended,security-and-quality - - # # If the analyze step fails for one of the languages you are analyzing with - # # "We were unable to automatically build your code", modify the matrix above - # # to set the build mode to "manual" for that language. Then modify this step - # # to build your code. - # # ℹī¸ Command-line programs to run using the OS shell. - # # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - # - if: matrix.build-mode == 'manual' - # shell: bash - # run: | - # echo 'If you are using a "manual" build mode for one or more of the' \ - # 'languages you are analyzing, replace this with the commands to build' \ - # 'your code, for example:' - # echo ' make bootstrap' - # echo ' make release' - # exit 1 - - # - name: Perform CodeQL Analysis - # uses: github/codeql-action/analyze@v3 - # with: - # category: "/language:${{matrix.language}}" diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml deleted file mode 100644 index d3d39ee..0000000 --- a/.github/workflows/maven-publish.yml +++ /dev/null @@ -1,34 +0,0 @@ -# This workflow will build a package using Maven and then publish it to GitHub packages when a release is created -# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path - -# name: Maven Package - -# on: - # release: - # types: [created] - -# jobs: - # build: - - # runs-on: ubuntu-latest - # permissions: - # contents: read - # packages: write - - # steps: - # - uses: actions/checkout@v4 - # - name: Set up JDK 21 - # uses: actions/setup-java@v3 - # with: - # java-version: '21' - # distribution: 'temurin' - # server-id: github # Value of the distributionManagement/repository/id field of the pom.xml - # settings-path: ${{ github.workspace }} # location for the settings.xml file - - # - name: Build with Maven - # run: mvn -B package --file pom.xml - - # - name: Publish to GitHub Packages Apache Maven - # run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml - # env: - # GITHUB_TOKEN: ${{ github.token }} diff --git a/primekit-essentials/src/main/java/com/shortthirdman/primekit/essentials/common/YearWeek.java b/primekit-essentials/src/main/java/com/shortthirdman/primekit/essentials/common/YearWeek.java new file mode 100644 index 0000000..ab26b18 --- /dev/null +++ b/primekit-essentials/src/main/java/com/shortthirdman/primekit/essentials/common/YearWeek.java @@ -0,0 +1,32 @@ +package com.shortthirdman.primekit.essentials.common; + +import java.text.MessageFormat; +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.chrono.Chronology; +import java.time.chrono.IsoChronology; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; +import java.util.Objects; + + +public record YearWeek(int year, int week) { + + public static YearWeek from(TemporalAccessor temporal) { + Objects.requireNonNull(temporal, "temporal"); + try { + if (!IsoChronology.INSTANCE.equals(Chronology.from(temporal))) { + temporal = LocalDate.from(temporal); + } + return new YearWeek(temporal.get(ChronoField.YEAR), temporal.get(ChronoField.ALIGNED_WEEK_OF_YEAR)); + } catch (DateTimeException ex) { + String mf = MessageFormat.format("Unable to obtain YearWeek from TemporalAccessor: {0} of type {1}", temporal, temporal.getClass().getName()); + throw new DateTimeException(mf, ex); + } + } + + @Override + public String toString() { + return MessageFormat.format("{0}-{1}", year, week); + } +} diff --git a/primekit-essentials/src/main/java/com/shortthirdman/primekit/essentials/common/util/DateUtils.java b/primekit-essentials/src/main/java/com/shortthirdman/primekit/essentials/common/util/DateUtils.java index 12edc91..6a9128c 100644 --- a/primekit-essentials/src/main/java/com/shortthirdman/primekit/essentials/common/util/DateUtils.java +++ b/primekit-essentials/src/main/java/com/shortthirdman/primekit/essentials/common/util/DateUtils.java @@ -1,5 +1,6 @@ package com.shortthirdman.primekit.essentials.common.util; +import com.shortthirdman.primekit.essentials.common.YearWeek; import org.apache.commons.lang3.StringUtils; import java.sql.Timestamp; @@ -11,6 +12,7 @@ import java.time.format.TextStyle; import java.time.temporal.ChronoField; import java.time.temporal.ChronoUnit; +import java.time.temporal.IsoFields; import java.util.*; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -21,7 +23,6 @@ * @apiNote Utility class for operations on {@link LocalDate}, {@link Date}, {@link LocalDateTime} * @author shortthirdman * @since 1.0 - * https://howtodoinjava.com/java/date-time/date-validation/ */ public final class DateUtils { @@ -392,4 +393,153 @@ public static String monthShortNameToFullName(String abbreviation) { return monthOptional.orElseThrow(IllegalArgumentException::new) .getDisplayName(TextStyle.FULL, Locale.getDefault()); } + + /** + * Get the number of quarters in between two dates + * @param start the start date + * @param end the end date + * @return the number of quarters + */ + public static Long quarterCount(LocalDate start, LocalDate end) { + if (start == null || end == null) { + throw new IllegalArgumentException("Start date or end date can not be null"); + } + + return IsoFields.QUARTER_YEARS.between(start, end); + } + + /** + * Get the quarter number in a short pattern-style + * @param date the {@link LocalDate} + * @return the quarter in the format "Q{1-4}" + */ + public static String shortQuarterNumber(LocalDate date) { + if (date == null) { + throw new IllegalArgumentException("Date can not be null"); + } + + return date.format(DateTimeFormatter.ofPattern("QQQ", Locale.ENGLISH)); + } + + /** + * Get the quarter number in a long pattern-style + * @param date the {@link LocalDate} + * @return the quarter in the format "n-th quarter" + */ + public static String longQuarterNumber(LocalDate date) { + if (date == null) { + throw new IllegalArgumentException("Date can not be null"); + } + + return date.format(DateTimeFormatter.ofPattern("QQQQ", Locale.ENGLISH)); + } + + /** + * Get quarter number for a given date + * @param date the {@link LocalDate} + * @return quarter number + */ + public static Integer getQuarterNumber(LocalDate date) { + if (date == null) { + throw new IllegalArgumentException("Date can not be null"); + } + + return date.get(IsoFields.QUARTER_OF_YEAR); + } + + /** + * Split date-time range into equal intervals + * @param start the start date + * @param end the end date + * @param n the intervals + * @return list of date-time + */ + public static List splitDateTimeRange(LocalDateTime start, LocalDateTime end, int n) { + if (start == null || end == null) { + throw new IllegalArgumentException("Start date or end date can not be null"); + } + + Duration range = Duration.between(start, end); + Duration interval = range.dividedBy(n - 1); + List listOfDates = new ArrayList<>(); + LocalDateTime timeline = start; + for (int i = 0; i < n - 1; i++) { + listOfDates.add(timeline); + timeline = timeline.plus(interval); + } + listOfDates.add(end); + return listOfDates; + } + + /** + * Split date-time range into days + * @param start the start date + * @param end the end date + * @return list of date + */ + public static List splitDateRangeIntoDays(LocalDate start, LocalDate end) { + if (start == null || end == null) { + throw new IllegalArgumentException("Start date or end date can not be null"); + } + + long numOfDaysBetween = ChronoUnit.DAYS.between(start, end); + return IntStream.iterate(0, i -> i + 1) + .limit(numOfDaysBetween) + .mapToObj(start::plusDays) + .collect(Collectors.toList()); + } + + /** + * Split date-time range into months + * @param start the start date + * @param end the end date + * @return list of year-month + */ + public static List splitDateRangeIntoMonths(LocalDate start, LocalDate end) { + if (start == null || end == null) { + throw new IllegalArgumentException("Start date or end date can not be null"); + } + + long numOfDaysBetween = ChronoUnit.MONTHS.between(start, end); + return IntStream.iterate(0, i -> i + 1) + .limit(numOfDaysBetween) + .mapToObj(i -> YearMonth.from(start.plusMonths(i))) + .collect(Collectors.toList()); + } + + /** + * Split date-time range into years + * @param start the start date + * @param end the end date + * @return the list of years + */ + public static List splitDateRangeIntoYears(LocalDate start, LocalDate end) { + if (start == null || end == null) { + throw new IllegalArgumentException("Start date or end date can not be null"); + } + + long numOfDaysBetween = ChronoUnit.YEARS.between(start, end); + return IntStream.iterate(0, i -> i + 1) + .limit(numOfDaysBetween) + .mapToObj(i -> Year.from(start.plusYears(i))) + .collect(Collectors.toList()); + } + + /** + * Split date-time range into weeks + * @param start the start date + * @param end the end date + * @return the list of year-week + */ + public static List splitDateRangeIntoWeeks(LocalDate start, LocalDate end) { + if (start == null || end == null) { + throw new IllegalArgumentException("Start date or end date can not be null"); + } + + long numOfDaysBetween = ChronoUnit.WEEKS.between(start, end); + return IntStream.iterate(0, i -> i + 1) + .limit(numOfDaysBetween) + .mapToObj(i -> YearWeek.from(start.plusWeeks(i))) + .collect(Collectors.toList()); + } } diff --git a/primekit-essentials/src/test/java/com/shortthirdman/primekit/essentials/DateUtilsTest.java b/primekit-essentials/src/test/java/com/shortthirdman/primekit/essentials/DateUtilsTest.java index 08e42ea..3be1773 100644 --- a/primekit-essentials/src/test/java/com/shortthirdman/primekit/essentials/DateUtilsTest.java +++ b/primekit-essentials/src/test/java/com/shortthirdman/primekit/essentials/DateUtilsTest.java @@ -5,10 +5,7 @@ import org.junit.jupiter.api.Test; import java.sql.Timestamp; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; +import java.time.*; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -198,4 +195,29 @@ public void givenMonthShortName_convertFullName() { assertEquals("January", DateUtils.monthShortNameToFullName("Jan")); assertThrows(IllegalArgumentException.class, () -> DateUtils.monthShortNameToFullName("Fut")); } + + @Test + public void givenLocalDate_getQuarter() { + LocalDate date = LocalDate.of(2024, Month.FEBRUARY, 19); + assertEquals(1, DateUtils.getQuarterNumber(date)); + assertThrows(IllegalArgumentException.class, () -> DateUtils.getQuarterNumber(null)); + } + + @Test + public void givenLocalDate_getFormattedQuarter() { + LocalDate date = LocalDate.of(2024, Month.FEBRUARY, 19); + assertEquals("Q1", DateUtils.shortQuarterNumber(date)); + assertEquals("1st quarter", DateUtils.longQuarterNumber(date)); + assertThrows(IllegalArgumentException.class, () -> DateUtils.shortQuarterNumber(null)); + assertThrows(IllegalArgumentException.class, () -> DateUtils.longQuarterNumber(null)); + } + + @Test + public void givenStartDate_givenEndDate_getQuarterCount() { + LocalDate start = LocalDate.of(2024, Month.FEBRUARY, 19); + LocalDate end = LocalDate.of(2024, Month.MAY, 5); + assertEquals(1, DateUtils.quarterCount(start, end)); + assertThrows(IllegalArgumentException.class, () -> DateUtils.quarterCount(null, end)); + assertThrows(IllegalArgumentException.class, () -> DateUtils.quarterCount(start, null)); + } }