All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.danielflower.crickam.scorer.MatchControl Maven / Gradle / Ivy

There is a newer version: 0.11.3
Show newest version
package com.danielflower.crickam.scorer;

import com.danielflower.crickam.scorer.events.MatchEvent;
import com.danielflower.crickam.scorer.events.MatchEventBuilder;
import com.danielflower.crickam.scorer.events.MatchEvents;
import com.danielflower.crickam.scorer.events.MatchStartingEvent;

import java.time.*;
import java.util.Iterator;
import java.util.function.Predicate;
import java.util.stream.Stream;

import static com.danielflower.crickam.scorer.Crictils.requireInRange;
import static java.util.Objects.requireNonNull;

/**
 * This class is the main entry point of the API.
 * 

To generate a cricket match model, create a new control with {@link #newMatch(MatchStartingEvent.Builder)}, * then feed in events to {@link #onEvent(MatchEventBuilder)}.

*

After events have been passed in, the current state can be found by calling {@link #match()} and historical * states can be found in {@link #history()}.

*

You can also look up specific events, and then get a match control object at that point in time using the * {@link #eventStream(Class)} and {@link #asAt(MatchEvent)} methods.

*

Because this object (and all objects it links to) are immutable, the {@code onEvent} methods return new * instances of this object rather than mutating the state. This means you are free to do things such as apply * events to existing objects and have the results be independent of other instances. For example, you can perform * "what if " scenarios (such as "what if a dropped catch had been caught") by looking * up the state as at some specific event with {@link #eventStream(Class)}, get the match control at that * point of time using {@link #asAt(MatchEvent)}, and then add new events using {@link #onEvent(MatchEventBuilder)} * to add the dismissal, and then access the new state at {@link #match()}. This would all happen on a copy of the * match control, so the original match data would not be changed.

*/ public final class MatchControl { private final ImmutableList ancestors; private final MatchEvent event; private final Match match; private MatchControl(ImmutableList ancestors, MatchEvent event, Match match) { this.ancestors = ancestors; this.event = event; this.match = match; } /** * Creates a new match control object * @param builder A builder for a new-match event * @return A match control object you can use to build up match state * @see MatchEvents#matchStarting(MatchType) */ public static MatchControl newMatch(MatchStartingEvent.Builder builder) { requireNonNull(builder, "builder"); MatchStartingEvent event = builder.build(null); Match match = Match.newMatch(event); return new MatchControl(ImmutableList.emptyList(), event, match); } /** * Applies the given event to the match, and returns a new instance of match control. *

The original object is not changed.

* @param builder A builder for a match event * @return A new Match Control object * @see MatchEvents MatchEvents class for a number of handy builder objects */ public MatchControl onEvent(MatchEventBuilder builder) { requireNonNull(builder, "builder"); MatchEvent event = builder.build(match()); Match newMatch = match().onEvent(event); ImmutableList newHistory = this.ancestors.add(this); MatchControl newMatchControl = new MatchControl(newHistory, event, newMatch); for (MatchEventBuilder childBuilder : event.generatedEvents()) { childBuilder.withGeneratedBy(event); newMatchControl = newMatchControl.onEvent(childBuilder); } return newMatchControl; } /** * @return The match at the current point in time */ public Match match() { return match; } /** * @return The last event that was applied */ public MatchEvent event() { return event; } /** * @return true if {@link #parent()} has a parent match control; false if this is the control created by {@link #newMatch(MatchStartingEvent.Builder)} * @see #parent() */ public boolean hasParent() { return !ancestors.isEmpty(); } /** * Returns the match control that is the parent of this one. *

Note that the parent state may not be the state of the match as at the time the API user last added an * event, as events may generate child events. To undo the last added user event, call {@link #atPreviousUserGeneratedEvent()} * instead.

* @return the parent of this control * @see #hasParent() * @see #atPreviousUserGeneratedEvent() * @see #atLastUserGeneratedEvent() * @throws IllegalStateException if this has no parent */ public MatchControl parent() { return ancestors.last().orElseThrow(() -> new IllegalStateException("Cannot get the parent of the first event")); } /** * Gets the current innings, or throws an exception if none are in progress. *

To get an optional innings, use {@code match().currentInnings()} instead.

* @return The current innings that's in progress * @throws IllegalStateException There is not an innings in progress */ public Innings currentInnings() { return match().currentInnings().orElseThrow(() -> new IllegalStateException("There is no innings in progress")); } /** * @see #eventStream(Class) * @return All the events and corresponding states in the order they occurred */ public ImmutableList history() { return ancestors.add(this); } /** * A convenient way to convert a local time (such as "17:30") into a {@code Instant} object. This * method uses the match's {@link Match#timeZone()} as the time zone and the last {@link MatchEvent#time()} to find * the date * @param hour The hour in the local time zone (between 0 and 23) * @param minute The minute in the local time zone (between 0 and 59) * @param second The second in the local time zone (between 0 and 59) * @return An instant based on the current match date and local time zone */ public Instant localTime(int hour, int minute, int second) { requireInRange("hour", hour, 0, 23); requireInRange("minute", minute, 0, 59); requireInRange("second", second, 0, 59); ZoneId zoneId = match().timeZone() .orElseThrow(() -> new UnsupportedOperationException("No time zone was set on the match (or on the venue) so local times cannot be calculated")) .toZoneId(); ImmutableList history = history(); for (int i = history.size() - 1; i >= 0; i--) { MatchControl er = history.get(i); if (er.event().time().isPresent()) { Instant lastKnownTime = er.event().time().get(); LocalDate date = LocalDate.ofInstant(lastKnownTime, zoneId); LocalTime time = LocalTime.of(hour, minute, second); LocalDateTime dateTime = LocalDateTime.of(date, time); return dateTime.atZone(zoneId).toInstant(); } } throw new UnsupportedOperationException("No events have a time recorded against them, so this method cannot know which date to use"); } /** * Gets the match control state as at the time of another event. *

One way to find an event is to {@link #eventStream(Class)}. In the following example, the state of * the match is found as at the end of the second innings:

*

     * MatchControl controlAtEndOfInnings2 = control.eventStream(InningsCompletedEvent.class)
     *         .filter(ice -> ice.inningsNumber() == 2)
     *         .findFirst()
     *         .map(e -> control.asAt(e))
     *         .orElseThrow();
     * 
* @param event The event to look up * @return The {@code MatchControl} as at the time that the event was added */ public MatchControl asAt(MatchEvent event) { requireNonNull(event, "event"); for (MatchControl matchControl : this.history()) { if (matchControl.event().equals(event)) { return matchControl; } } throw new IllegalArgumentException("No matching event found on this match"); } /** * Returns a stream of the events that have happened on this match. *

Note that given a single event, the entire state of that match at that point can be found by * using the {@link #asAt(MatchEvent)} method.

* @param eventClass The type of event to filter by * @param The type of stream that will be returned * @return A stream */ public Stream eventStream(Class eventClass) { Class clazz = requireNonNull(eventClass, "eventClass"); return history().stream() .filter(c -> c.event().getClass().equals(clazz)) .map(MatchControl::event) .map(eventClass::cast); } /** * A predicate that returns true if the innings number of the match after applying the event is the same as the * given innings number. *

This is designed to be used to filter the result of {@link MatchControl#history()}

* @param innings The innings (at any point of time in that innings) that you are searching for * @return A predicate that can be used in a stream filter */ public static Predicate sameInnings(Innings innings) { return mc -> mc.match().currentInnings().isPresent() && mc.match().currentInnings().get().inningsNumber() == innings.inningsNumber(); } /** * Some events auto generate child events, in which case {@link #event()} would not link to the last event * added by the API user. This method finds the most recent match control state generated by a user event. * @return The match as at last event that was generated by the API user * @see #atPreviousUserGeneratedEvent() */ public MatchControl atLastUserGeneratedEvent() { Iterator all = history().reverseIterator(); while (all.hasNext()) { MatchControl control = all.next(); if (control.event().generatedBy().isEmpty()) { return control; } } // should not be possible to get here as the first event is always user generated throw new IllegalStateException("Cannot call this method on an empty match"); } /** * Returns the match as at the state when the second to last event was added by the API user. *

This can be used to undo applied events by calling this method on a match control, and then calling * {@link #onEvent(MatchEventBuilder)} with a new event on the parent control.

*

This differs from {@link #atLastUserGeneratedEvent()} in that that method may return * the current state of the match.

* @return The match with the last user generated event undone. * @see #atLastUserGeneratedEvent() */ public MatchControl atPreviousUserGeneratedEvent() { return atLastUserGeneratedEvent().parent().atLastUserGeneratedEvent(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy