Skip to content

Commit

Permalink
Tidying.
Browse files Browse the repository at this point in the history
  • Loading branch information
grahamkirby committed Jan 5, 2025
1 parent 65ea2a2 commit fa5e3c5
Show file tree
Hide file tree
Showing 42 changed files with 402 additions and 351 deletions.
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
package org.grahamkirby.race_timing.common;

/** Enumerates options for runner/team completion status for race or series. */
public enum CompletionStatus {

/**
* Completed race or series.
*/
/** Completed race or series. */
COMPLETED,

/**
* Did Not Start: runner appears in entry but no finish(es) recorded.
*/
/** Did Not Start: appears in entry list but no finish(es) recorded. */
DNS,

/**
* Did Not Finish: did not complete all legs or sufficient component races.
* Did Not Finish: did not complete all legs (relay race) or sufficient component races (series race).
*/
DNF
}
140 changes: 87 additions & 53 deletions src/main/java/org/grahamkirby/race_timing/common/Normalisation.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,59 +5,70 @@
import java.util.Map;
import java.util.Set;

@SuppressWarnings("StringTemplateMigration")
public class Normalisation {

private static final int SECONDS_PER_HOUR = 3600;
private static final int SECONDS_PER_MINUTE = 60;
private static final double NANOSECONDS_PER_SECOND = 1000000000.0;
private static final double NANOSECONDS_PER_SECOND = 1_000_000_000.0;

private static final Set<Character> WORD_SEPARATORS = Set.of(' ', '-', '\'', '’');
private static final Map<String, String> REMOVE_DOUBLE_SPACES = Map.of(" ", " ");

//////////////////////////////////////////////////////////////////////////////////////////////////

private final Race race;

public Normalisation(final Race race) {
this.race = race;
}

static String getFirstName(final String name) {
return name.split(" ")[0];
}

static String getLastName(final String name) {

return Arrays.stream(name.split(" ")).toList().getLast();
}
//////////////////////////////////////////////////////////////////////////////////////////////////

/** Cleans name by removing extra whitespace and converting to title case, unless present
* in stop list file. */
public String cleanRunnerName(final String name) {

// Remove extra whitespace.
final String step1 = replaceAllMapEntries(name, REMOVE_DOUBLE_SPACES);
final String step2 = step1.strip();

// Convert to title case, unless present in stop list.
return toTitleCase(step2);
}

/** Cleans name by removing extra whitespace and normalising if present in normalisation file,
* otherwise converting to title case, unless present in stop list file. */
public String cleanClubOrTeamName(final String name) {

// Remove extra whitespace.
final String step1 = replaceAllMapEntries(name, REMOVE_DOUBLE_SPACES);
final String step2 = step1.strip();

// Check normalisation list (case insensitive).
// Check normalisation list (which is case insensitive for keys).
if (race.normalised_club_names.containsKey(step2)) return race.normalised_club_names.get(step2);

// Convert to title case, unless present in stop list.
return toTitleCase(step2);
}

/** Replaces any accented characters with HTML entity codes. */
public String htmlEncode(final String s) {

return replaceAllMapEntries(s, race.normalised_html_entities);
}

/** Gets the first element of the array resulting from splitting the given name on the space character. */
static String getFirstName(final String name) {
return name.split(" ")[0];
}

/** Gets the last element of the array resulting from splitting the given name on the space character. */
static String getLastName(final String name) {
return Arrays.stream(name.split(" ")).toList().getLast();
}

//////////////////////////////////////////////////////////////////////////////////////////////////

/** Converts the given string to title case, ignoring any words present in the stop word file. */
private String toTitleCase(final String input) {

final String s = lookupInStopWords(input);
Expand All @@ -66,12 +77,28 @@ private String toTitleCase(final String input) {
final StringBuilder result = new StringBuilder();

while (result.length() < input.length())
processNextWord(input, result);
addNextWord(input, result);

return result.toString();
}

private void processNextWord(final String input, final StringBuilder builder) {
/** Checks whether the given word is present in the stop word file, first with exact match
* and then case insensitive. Returns the matching word if found, otherwise null. */
private String lookupInStopWords(final String word) {

// Try case sensitive match first.
if (race.capitalisation_stop_words.contains(word)) return word;

// Try case insensitive match.
return race.capitalisation_stop_words.stream().
filter(w -> w.equalsIgnoreCase(word)).
findFirst().
orElse(null);
}

/** Finds the next word in the given input not already added to the builder, and adds it
* after converting to title case. */
private void addNextWord(final String input, final StringBuilder builder) {

char separator = 0;
int i;
Expand All @@ -89,6 +116,7 @@ private void processNextWord(final String input, final StringBuilder builder) {
if (separator > 0) builder.append(separator);
}

/** Converts the given word to title case, unless present in stop word file. */
private String toTitleCaseWord(final String word) {

final String s = lookupInStopWords(word);
Expand All @@ -100,85 +128,91 @@ private String toTitleCaseWord(final String word) {
return Character.toUpperCase(word.charAt(0)) + word.substring(1).toLowerCase();
}

private String lookupInStopWords(final String word) {

// Try case sensitive match first.
if (race.capitalisation_stop_words.contains(word)) return word;

// Try case insensitive match.
return race.capitalisation_stop_words.stream().
filter(w -> w.equalsIgnoreCase(word)).
findFirst().
orElse(null);
}

/** Tests whether given word has title case. */
@SuppressWarnings("TypeMayBeWeakened")
private static boolean isTitleCase(final String input) {
private static boolean isTitleCase(final String word) {

return !Character.isLowerCase(input.charAt(0)) &&
input.chars().boxed().skip(1).noneMatch(Character::isUpperCase);
return !Character.isLowerCase(word.charAt(0)) &&
word.chars().boxed().skip(1).noneMatch(Character::isUpperCase);
}

/** For each map entry, searches for instances of the key in the given string (case insensitive)
* and replaces each one with the corresponding value. */
private static String replaceAllMapEntries(final String s, final Map<String, String> normalisation_map) {

String result = s;

for (final Map.Entry<String, String> entry : normalisation_map.entrySet()) {
for (final Map.Entry<String, String> entry : normalisation_map.entrySet())
// "(?i)" specifies case insensitive map lookup.
result = result.replaceAll(STR."(?i)\{entry.getKey()}", entry.getValue());

final String value = entry.getValue();
result = result.replaceAll(STR."(?i)\{entry.getKey()}", value);
}
return result;
}

public static Duration parseTime(final String element) {
/** Parses the given time string, trying both colon and full stop as separators. */
public static Duration parseTime(final String time) {

try {
return parseTime(element, ":");
return parseTime(time, ":");
} catch (final RuntimeException _) {
return parseTime(element, "\\.");
return parseTime(time, "\\.");
}
}

private static Duration parseTime(String element, final String separator) {
/** Parses the given time in format hours/minutes/seconds or minutes/seconds, using the given separator. */
private static Duration parseTime(String time, final String separator) {

time = time.strip();

element = element.strip();
if (element.startsWith(separator)) element = STR."0\{element}";
if (element.endsWith(separator)) element = STR."\{element}0";
// Deal with missing hours or seconds component.
if (time.startsWith(separator)) time = STR."0\{time}";
if (time.endsWith(separator)) time = STR."\{time}0";

try {
final String[] parts = element.split(separator);
final String[] parts = time.split(separator);

// Construct ISO-8601 duration format.
return Duration.parse(STR."PT\{hours(parts)}\{minutes(parts)}\{seconds(parts)}");

} catch (final RuntimeException _) {
throw new RuntimeException(STR."illegal time: \{element}");
throw new RuntimeException(STR."illegal time: \{time}");
}
}

/** Formats the given duration into a string in HH:MM:SS.SSS format, omitting fractional trailing zeros. */
public static String format(final Duration duration) {

final long seconds = duration.getSeconds();
final int n = duration.getNano();
final long total_seconds = duration.getSeconds();

String result = String.format("0%d:%02d:%02d", seconds / SECONDS_PER_HOUR, (seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE, (seconds % SECONDS_PER_MINUTE));
if (n > 0) {
final double fractional_seconds = n / NANOSECONDS_PER_SECOND;
result += String.format("%1$,.3f", fractional_seconds).substring(1);
while (result.endsWith("0")) result = result.substring(0, result.length() - 1);
}
return result;
final long hours = total_seconds / SECONDS_PER_HOUR;
final long minutes = (total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
final long seconds = total_seconds % SECONDS_PER_MINUTE;

return String.format("%02d:%02d:%02d", hours, minutes, seconds) + formatFractionalPart(duration.getNano());
}

private static String formatFractionalPart(final int fractional_seconds_as_nanoseconds) {

if (fractional_seconds_as_nanoseconds == 0) return "";

final double fractional_seconds = fractional_seconds_as_nanoseconds / NANOSECONDS_PER_SECOND;

// Round to 3 decimal places.
final String formatted_fractional_seconds = String.format("%1$,.3f", fractional_seconds);

// Omit the zero preceding the decimal point, and trailing zeros.
return formatted_fractional_seconds.replaceAll("^0|0+$", "");
}

private static String hours(final String[] parts) {
return parts.length > 2 ? STR."\{parts[0]}H" : "";
}

private static String minutes(final String[] parts) {
return (parts.length > 2 ? parts[1] : parts[0]) + "M";
return STR."\{parts.length > 2 ? parts[1] : parts[0]}M";
}

private static String seconds(final String[] parts) {
return (parts.length > 2 ? parts[2] : parts[1]) + "S";
return STR."\{parts.length > 2 ? parts[2] : parts[1]}S";
}
}
50 changes: 6 additions & 44 deletions src/main/java/org/grahamkirby/race_timing/common/Race.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,73 +32,36 @@
import java.util.*;
import java.util.function.Predicate;

/** Base class for all types of race. */
@SuppressWarnings("IncorrectFormatting")
public abstract class Race {

/** Comment symbol used within configuration files. */
public static final String COMMENT_SYMBOL = "#";

//////////////////////////////////////////////////////////////////////////////////////////////////

// Configuration file keys.

// All races.
public static final String KEY_YEAR = "YEAR";
public static final String KEY_RACE_NAME_FOR_RESULTS = "RACE_NAME_FOR_RESULTS";
public static final String KEY_RACE_NAME_FOR_FILENAMES = "RACE_NAME_FOR_FILENAMES";
public static final String KEY_CATEGORIES_ENTRY_PATH = "CATEGORIES_ENTRY_PATH";
public static final String KEY_CATEGORIES_PRIZE_PATH = "CATEGORIES_PRIZE_PATH";
private static final String KEY_ENTRY_MAP_PATH = "ENTRY_MAP_PATH";
private static final String KEY_NORMALISED_CLUB_NAMES_PATH = "NORMALISED_CLUB_NAMES_PATH";
private static final String KEY_CAPITALISATION_STOP_WORDS_PATH = "CAPITALISATION_STOP_WORDS_PATH";
private static final String KEY_NORMALISED_HTML_ENTITIES_PATH = "NORMALISED_HTML_ENTITIES_PATH";

// Single race.
public static final String KEY_ENTRIES_PATH = "ENTRIES_PATH";
public static final String KEY_RAW_RESULTS_PATH = "RAW_RESULTS_PATH";
public static final String KEY_RESULTS_PATH = "RESULTS_PATH";
public static final String KEY_CATEGORIES_ENTRY_PATH = "CATEGORIES_ENTRY_PATH";
public static final String KEY_CATEGORIES_PRIZE_PATH = "CATEGORIES_PRIZE_PATH";
protected static final String KEY_DNF_FINISHERS = "DNF_FINISHERS";

// Individual race.
protected static final String KEY_MEDIAN_TIME = "MEDIAN_TIME";

// Relay race.
protected static final String KEY_GENDER_ELIGIBILITY_MAP_PATH = "GENDER_ELIGIBILITY_MAP_PATH";
public static final String KEY_ANNOTATIONS_PATH = "ANNOTATIONS_PATH";
public static final String KEY_PAPER_RESULTS_PATH = "PAPER_RESULTS_PATH";
protected static final String KEY_NUMBER_OF_LEGS = "NUMBER_OF_LEGS";
protected static final String KEY_PAIRED_LEGS = "PAIRED_LEGS";
protected static final String KEY_INDIVIDUAL_LEG_STARTS = "INDIVIDUAL_LEG_STARTS";
protected static final String KEY_MASS_START_ELAPSED_TIMES = "MASS_START_ELAPSED_TIMES";
protected static final String KEY_START_OFFSET = "START_OFFSET";

// Series race.
public static final String KEY_RACES = "RACES";
protected static final String KEY_NUMBER_OF_RACES_IN_SERIES = "NUMBER_OF_RACES_IN_SERIES";
protected static final String KEY_MINIMUM_NUMBER_OF_RACES = "MINIMUM_NUMBER_OF_RACES";

// Grand Prix race.
protected static final String KEY_RACE_CATEGORIES_PATH = "RACE_CATEGORIES_PATH";
protected static final String KEY_QUALIFYING_CLUBS = "QUALIFYING_CLUBS";

// Midweek race.
protected static final String KEY_SCORE_FOR_FIRST_PLACE = "SCORE_FOR_FIRST_PLACE";

// Minitour race.
public static final String KEY_WAVE_START_OFFSETS = "WAVE_START_OFFSETS";
public static final String KEY_SECOND_WAVE_CATEGORIES = "SECOND_WAVE_CATEGORIES";
public static final String KEY_TIME_TRIAL_RACE = "TIME_TRIAL_RACE";
public static final String KEY_TIME_TRIAL_STARTS = "TIME_TRIAL_STARTS";
public static final String KEY_SELF_TIMED = "SELF_TIMED";

//////////////////////////////////////////////////////////////////////////////////////////////////

/** Platform-specific line separator used in creating output files. */
public static final String LINE_SEPARATOR = System.lineSeparator();

public static final String SUFFIX_CSV = ".csv";
public static final String SUFFIX_PDF = ".pdf";

/** Used when a result is recorded without a bib number. */
public static final int UNKNOWN_BIB_NUMBER = 0;
public static final int UNKNOWN_LEG_NUMBER = 0;
protected static final int UNKNOWN_RACE_POSITION = 0;
private static final int PRIZE_CATEGORY_GROUP_NAME_INDEX = 6;

Expand Down Expand Up @@ -207,7 +170,6 @@ protected void printNotes() throws IOException {
protected void printCombined() throws IOException {

output_HTML.printCombined();
output_HTML.printCreditLink();
}

public Path getPath(final String path) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ protected RaceResult(final Race race) {
protected abstract String getIndividualRunnerName();
public abstract int comparePerformanceTo(final RaceResult other);
public abstract CompletionStatus getCompletionStatus();
public abstract boolean shouldDisplayPosition();
public abstract boolean shouldBeDisplayedInResults();
public abstract boolean shouldDisplayPosition();
public abstract EntryCategory getCategory();
}
Loading

0 comments on commit fa5e3c5

Please sign in to comment.