Skip to content

Commit

Permalink
[#1279] Provide a way to run analysis for a moving window (#1280)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
niqiukun authored Aug 2, 2020
1 parent 6020694 commit d02d735
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 3 deletions.
14 changes: 14 additions & 0 deletions docs/ug/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

<box type="info" seamless>

* 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`.
</box>
<!-- ------------------------------------------------------------------------------------------------------ -->

### `--repos`, `-r`

**`--repos REPO_LOCATION`**: Specifies which repositories to analyze.
Expand Down
58 changes: 55 additions & 3 deletions src/main/java/reposense/parser/ArgsParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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"};
Expand All @@ -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("");
Expand Down Expand Up @@ -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("*")
Expand Down Expand Up @@ -175,8 +185,21 @@ public static CliArguments parse(String[] args) throws HelpScreenException, Pars
Optional<Date> 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<Integer> 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<String> locations = results.get(REPO_FLAGS[0]);
List<FileType> formats = FileType.convertFormatStringsToFileTypes(results.get(FORMAT_FLAGS[0]));
boolean isStandaloneConfigIgnored = results.get(IGNORE_FLAGS[0]);
Expand Down Expand Up @@ -230,6 +253,35 @@ private static Date getDateMinusAMonth(Optional<Date> 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<Date> 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<Date> 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.
*/
Expand Down
45 changes: 45 additions & 0 deletions src/main/java/reposense/parser/PeriodArgumentType.java
Original file line number Diff line number Diff line change
@@ -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<Optional<Integer>> {
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<Integer> 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);
}
}
}
45 changes: 45 additions & 0 deletions src/test/java/reposense/parser/ArgsParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
11 changes: 11 additions & 0 deletions src/test/java/reposense/util/InputBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit d02d735

Please sign in to comment.