From d02d73514e957569c88f16af1548c8909dc0151a Mon Sep 17 00:00:00 2001 From: Ni Tianzhen Date: Sun, 2 Aug 2020 10:03:29 +0800 Subject: [PATCH] [#1279] Provide a way to run analysis for a moving window (#1280) Some users might want to monitor repositories for a moving window rather than from a specific date. We can give a CLI option that runs the analysis for a period of specific length e.g., `--period 7d` runs the analysis for the last 7 days, and `--period 1w` runs the analysis for the last 1 week. Period can combine with either since date or until date, but not both. If both not supplied, it will be the period before the current date. --- docs/ug/cli.md | 14 +++++ .../java/reposense/parser/ArgsParser.java | 58 ++++++++++++++++++- .../reposense/parser/PeriodArgumentType.java | 45 ++++++++++++++ .../java/reposense/parser/ArgsParserTest.java | 45 ++++++++++++++ .../java/reposense/util/InputBuilder.java | 11 ++++ 5 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 src/main/java/reposense/parser/PeriodArgumentType.java diff --git a/docs/ug/cli.md b/docs/ug/cli.md index ddd28b2857..5531f3e1b9 100644 --- a/docs/ug/cli.md +++ b/docs/ug/cli.md @@ -89,6 +89,20 @@ This flag overrides the `Ignore standalone config` field in the CSV config file. +### `--period`, `-p` + +**`--period PERIOD`**: Specifies the period of analysis window. +* Parameter `PERIOD`: The period of analysis window, in the format `nd` (for n days) or `nw` (for n weeks). It is used to calculate end date if only start date is specified, or calculate end date if only start date is specified. +* Alias: `-p` +* Example: `--period 30d` or `--period 4w` + + + +* If both start date and end date are not specified, the date of generating the report will be taken as the end date. +* Cannot be used with both `--since` and `--until`. + + + ### `--repos`, `-r` **`--repos REPO_LOCATION`**: Specifies which repositories to analyze. diff --git a/src/main/java/reposense/parser/ArgsParser.java b/src/main/java/reposense/parser/ArgsParser.java index 7acfd98820..01c9611100 100644 --- a/src/main/java/reposense/parser/ArgsParser.java +++ b/src/main/java/reposense/parser/ArgsParser.java @@ -41,6 +41,7 @@ public class ArgsParser { public static final String[] OUTPUT_FLAGS = new String[]{"--output", "-o"}; public static final String[] SINCE_FLAGS = new String[]{"--since", "-s"}; public static final String[] UNTIL_FLAGS = new String[]{"--until", "-u"}; + public static final String[] PERIOD_FLAGS = new String[]{"--period", "-p"}; public static final String[] FORMAT_FLAGS = new String[]{"--formats", "-f"}; public static final String[] IGNORE_FLAGS = new String[]{"--ignore-standalone-config", "-i"}; public static final String[] TIMEZONE_FLAGS = new String[]{"--timezone", "-t"}; @@ -53,9 +54,11 @@ public class ArgsParser { "RepoSense is a contribution analysis tool for Git repositories."; private static final String MESSAGE_HEADER_MUTEX = "mutual exclusive arguments"; private static final String MESSAGE_SINCE_DATE_LATER_THAN_UNTIL_DATE = - "\"Since Date\" cannot be later than \"Until Date\""; + "\"Since Date\" cannot be later than \"Until Date\"."; private static final String MESSAGE_SINCE_DATE_LATER_THAN_TODAY_DATE = "\"Since Date\" must not be later than today's date."; + private static final String MESSAGE_HAVE_SINCE_DATE_UNTIL_DATE_AND_PERIOD = + "\"Since Date\", \"Until Date\", and \"Period\" cannot be applied together."; private static final String MESSAGE_USING_DEFAULT_CONFIG_PATH = "Config path not provided, using the config folder as default."; private static final Path EMPTY_PATH = Paths.get(""); @@ -120,6 +123,13 @@ private static ArgumentParser getArgumentParser() { .setDefault(Optional.empty()) .help("The date to stop filtering."); + parser.addArgument(PERIOD_FLAGS) + .dest(PERIOD_FLAGS[0]) + .metavar("PERIOD") + .type(new PeriodArgumentType()) + .setDefault(Optional.empty()) + .help("The number of days of the filtering window."); + parser.addArgument(FORMAT_FLAGS) .dest(FORMAT_FLAGS[0]) .nargs("*") @@ -175,8 +185,21 @@ public static CliArguments parse(String[] args) throws HelpScreenException, Pars Optional cliUntilDate = results.get(UNTIL_FLAGS[0]); boolean isSinceDateProvided = cliSinceDate.isPresent(); boolean isUntilDateProvided = cliUntilDate.isPresent(); - Date sinceDate = cliSinceDate.orElse(getDateMinusAMonth(cliUntilDate)); - Date untilDate = cliUntilDate.orElse(getCurrentDate()); + Optional cliPeriod = results.get(PERIOD_FLAGS[0]); + boolean isPeriodProvided = cliPeriod.isPresent(); + if (isSinceDateProvided && isUntilDateProvided && isPeriodProvided) { + throw new ParseException(MESSAGE_HAVE_SINCE_DATE_UNTIL_DATE_AND_PERIOD); + } + Date sinceDate = cliSinceDate.orElse(isPeriodProvided + ? getDateMinusNDays(cliUntilDate, cliPeriod.get()) + : getDateMinusAMonth(cliUntilDate)); + Date currentDate = getCurrentDate(); + Date untilDate = cliUntilDate.orElse((isSinceDateProvided && isPeriodProvided) + ? getDatePlusNDays(cliSinceDate, cliPeriod.get()) + : currentDate); + untilDate = untilDate.compareTo(currentDate) < 0 + ? untilDate + : currentDate; List locations = results.get(REPO_FLAGS[0]); List formats = FileType.convertFormatStringsToFileTypes(results.get(FORMAT_FLAGS[0])); boolean isStandaloneConfigIgnored = results.get(IGNORE_FLAGS[0]); @@ -230,6 +253,35 @@ private static Date getDateMinusAMonth(Optional cliUntilDate) { return cal.getTime(); } + /** + * Returns a {@code Date} that is {@code numOfDays} before {@code cliUntilDate} (if present) or one month before + * report generation date otherwise. + */ + private static Date getDateMinusNDays(Optional cliUntilDate, int numOfDays) { + Calendar cal = Calendar.getInstance(); + cliUntilDate.ifPresent(cal::setTime); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + cal.add(Calendar.DATE, -numOfDays + 1); + return cal.getTime(); + } + + /** + * Returns a {@code Date} that is {@code numOfDays} after {@code cliSinceDate} (if present). + */ + private static Date getDatePlusNDays(Optional cliSinceDate, int numOfDays) { + Calendar cal = Calendar.getInstance(); + cliSinceDate.ifPresent(cal::setTime); + cal.set(Calendar.HOUR_OF_DAY, 23); + cal.set(Calendar.MINUTE, 59); + cal.set(Calendar.SECOND, 59); + cal.set(Calendar.MILLISECOND, 0); + cal.add(Calendar.DATE, numOfDays - 1); + return cal.getTime(); + } + /** * Returns current date with time set to 23:59:59. */ diff --git a/src/main/java/reposense/parser/PeriodArgumentType.java b/src/main/java/reposense/parser/PeriodArgumentType.java new file mode 100644 index 0000000000..0c0ca5bab0 --- /dev/null +++ b/src/main/java/reposense/parser/PeriodArgumentType.java @@ -0,0 +1,45 @@ +package reposense.parser; + +import java.util.Optional; +import java.util.regex.Pattern; + +import net.sourceforge.argparse4j.inf.Argument; +import net.sourceforge.argparse4j.inf.ArgumentParser; +import net.sourceforge.argparse4j.inf.ArgumentParserException; +import net.sourceforge.argparse4j.inf.ArgumentType; + +/** + * Verifies and parses a string-formatted period to an integer. + */ +public class PeriodArgumentType implements ArgumentType> { + private static final String PARSE_EXCEPTION_MESSAGE_NOT_IN_NUMERIC = + "Invalid format. Period must be in the format of nd (n days) or nw (n weeks), " + + "where n is a number greater than 0."; + private static final String PARSE_EXCEPTION_MESSAGE_SMALLER_THAN_ZERO = + "Invalid format. Period must be greater than 0."; + private static final String PARSE_EXCEPTION_MESSAGE_NUMBER_TOO_LARGE = + "Invalid format. Input number may be too large."; + private static final Pattern PERIOD_PATTERN = Pattern.compile("[0-9]+[dw]"); + + @Override + public Optional convert(ArgumentParser parser, Argument arg, String value) throws ArgumentParserException { + if (!PERIOD_PATTERN.matcher(value).matches()) { + throw new ArgumentParserException( + String.format(PARSE_EXCEPTION_MESSAGE_NOT_IN_NUMERIC, value), parser); + } + + int multiplier = value.substring(value.length() - 1).equals("d") ? 1 : 7; + try { + int convertedValue = Integer.parseInt(value.substring(0, value.length() - 1)) * multiplier; + if (convertedValue <= 0) { + throw new ArgumentParserException( + String.format(PARSE_EXCEPTION_MESSAGE_SMALLER_THAN_ZERO, value), parser); + } + + return Optional.of(convertedValue); + } catch (NumberFormatException e) { + throw new ArgumentParserException( + String.format(PARSE_EXCEPTION_MESSAGE_NUMBER_TOO_LARGE, value), parser); + } + } +} diff --git a/src/test/java/reposense/parser/ArgsParserTest.java b/src/test/java/reposense/parser/ArgsParserTest.java index 5a1a0ca629..618b080195 100644 --- a/src/test/java/reposense/parser/ArgsParserTest.java +++ b/src/test/java/reposense/parser/ArgsParserTest.java @@ -360,6 +360,30 @@ public void untilDate_withExtraTime_success() throws Exception { Assert.assertEquals(expectedUntilDate, cliArguments.getUntilDate()); } + @Test + public void period_inDaysWithSinceDate_success() throws Exception { + String input = DEFAULT_INPUT_BUILDER + .addSinceDate("01/07/2017") + .addPeriod("2d") + .build(); + CliArguments cliArguments = ArgsParser.parse(translateCommandline(input)); + Assert.assertTrue(cliArguments instanceof ConfigCliArguments); + Date expectedUntilDate = TestUtil.getUntilDate(2017, Calendar.JULY, 2); + Assert.assertEquals(expectedUntilDate, cliArguments.getUntilDate()); + } + + @Test + public void period_inWeeksWithUntilDate_success() throws Exception { + String input = DEFAULT_INPUT_BUILDER + .addUntilDate("14/07/2017") + .addPeriod("2w") + .build(); + CliArguments cliArguments = ArgsParser.parse(translateCommandline(input)); + Assert.assertTrue(cliArguments instanceof ConfigCliArguments); + Date expectedSinceDate = TestUtil.getSinceDate(2017, Calendar.JULY, 1); + Assert.assertEquals(expectedSinceDate, cliArguments.getSinceDate()); + } + @Test public void formats_inAlphanumeric_success() throws Exception { String input = DEFAULT_INPUT_BUILDER.addFormats("java js css 7z").build(); @@ -533,6 +557,27 @@ public void sinceDate_laterThanUntilDate_throwsParseException() throws Exception ArgsParser.parse(translateCommandline(input)); } + @Test(expected = ParseException.class) + public void period_withBothSinceDateAndUntilDate_throwsParseException() throws Exception { + String input = DEFAULT_INPUT_BUILDER.addPeriod("18d") + .addSinceDate("30/11/2017") + .addUntilDate("01/12/2017") + .build(); + ArgsParser.parse(translateCommandline(input)); + } + + @Test(expected = ParseException.class) + public void period_notNumeric_throwsParseExcpetion() throws Exception { + String input = DEFAULT_INPUT_BUILDER.addPeriod("abcd").build(); + ArgsParser.parse(translateCommandline(input)); + } + + @Test(expected = ParseException.class) + public void period_isZero_throwsParseExcpetion() throws Exception { + String input = DEFAULT_INPUT_BUILDER.addPeriod("0w").build(); + ArgsParser.parse(translateCommandline(input)); + } + @Test(expected = ParseException.class) public void formats_notInAlphanumeric_throwsParseException() throws Exception { String input = DEFAULT_INPUT_BUILDER.addFormats(".java").build(); diff --git a/src/test/java/reposense/util/InputBuilder.java b/src/test/java/reposense/util/InputBuilder.java index 1022353ebc..38d03c24ea 100644 --- a/src/test/java/reposense/util/InputBuilder.java +++ b/src/test/java/reposense/util/InputBuilder.java @@ -112,6 +112,17 @@ public InputBuilder addUntilDate(String date) { return this; } + /** + * Adds the period flag with the {@code period} as argument to the input. + * This method should only be called once in one build. + * + * @param period The period. + */ + public InputBuilder addPeriod(String period) { + input.append(ArgsParser.PERIOD_FLAGS[0] + WHITESPACE + period + WHITESPACE); + return this; + } + /** * Adds the format flag with the {@code formats} as argument to the input. * This method should only be called once in one build.