def test_irange_key(): values = sorted(range(100), key=modulo) for load in range(5, 16): slt = SortedKeyList(range(100), key=modulo) slt._reset(load) for start in range(10): for end in range(start, 10): temp = list(slt.irange_key(start, end)) assert temp == values[(start * 10):((end + 1) * 10)] temp = list(slt.irange_key(start, end, reverse=True)) assert temp == values[(start * 10):((end + 1) * 10)][::-1] for start in range(10): for end in range(start, 10): temp = list(slt.irange_key(start, end, inclusive=(True, False))) assert temp == values[(start * 10):(end * 10)] for start in range(10): for end in range(start, 10): temp = list(slt.irange_key(start, end, (False, True))) assert temp == values[((start + 1) * 10):((end + 1) * 10)] for start in range(10): for end in range(start, 10): temp = list( slt.irange_key(start, end, inclusive=(False, False))) assert temp == values[((start + 1) * 10):(end * 10)] for start in range(10): temp = list(slt.irange_key(min_key=start)) assert temp == values[(start * 10):] for end in range(10): temp = list(slt.irange_key(max_key=end)) assert temp == values[:(end + 1) * 10]
def test_irange_key(): values = sorted(range(100), key=modulo) for load in range(5, 16): slt = SortedKeyList(range(100), key=modulo) slt._reset(load) for start in range(10): for end in range(start, 10): temp = list(slt.irange_key(start, end)) assert temp == values[(start * 10):((end + 1) * 10)] temp = list(slt.irange_key(start, end, reverse=True)) assert temp == values[(start * 10):((end + 1) * 10)][::-1] for start in range(10): for end in range(start, 10): temp = list(slt.irange_key(start, end, inclusive=(True, False))) assert temp == values[(start * 10):(end * 10)] for start in range(10): for end in range(start, 10): temp = list(slt.irange_key(start, end, (False, True))) assert temp == values[((start + 1) * 10):((end + 1) * 10)] for start in range(10): for end in range(start, 10): temp = list(slt.irange_key(start, end, inclusive=(False, False))) assert temp == values[((start + 1) * 10):(end * 10)] for start in range(10): temp = list(slt.irange_key(min_key=start)) assert temp == values[(start * 10):] for end in range(10): temp = list(slt.irange_key(max_key=end)) assert temp == values[:(end + 1) * 10]
class FreshPondSim: def __init__(self, distance, start_time, end_time, entrances, entrance_weights, rand_velocities_and_distances_func, entrance_rate, entrance_rate_integral=None, entrance_rate_integral_inverse=None, interpolate_rate=True, interpolate_rate_integral=True, interpolate_res=None, snap_exit=True): assert_positive_real(distance, 'distance') assert_real(start_time, 'start_time') assert_real(end_time, 'end_time') if not (start_time < end_time): raise ValueError(f"start_time should be less than end_time") assert len(entrances) == len(entrance_weights) self.start_time = start_time self.end_time = end_time self.dist_around = distance self.entrances = entrances self.entrance_weights = entrance_weights self.rand_velocities_and_distances = rand_velocities_and_distances_func self._snap_exit = snap_exit if interpolate_rate or interpolate_rate_integral: if interpolate_res is None: raise ValueError("Specify interpolate_res for interpolation") if interpolate_rate: self.entrance_rate = DynamicBoundedInterpolator( entrance_rate, start_time, end_time, interpolate_res) else: self.entrance_rate = entrance_rate if interpolate_rate_integral: # Want to interplate the integral function if entrance_rate_integral is None: # No integral function given # Do numerical integration and interpolate to speed it up def integral_func(t): y, abserr = integrate.quad(entrance_rate, start_time, t) return y self.entrance_rate_integral = DynamicBoundedInterpolator( integral_func, start_time, end_time, interpolate_res) else: # Integral function was provided # Use the provided rate integral function but interpolate it self.entrance_rate_integral = DynamicBoundedInterpolator( entrance_rate_integral, start_time, end_time, interpolate_res) else: # Don't want to interpolate the integral function # If entrance_rate_integral is not None (i.e. is provided) then # that function will be used as the rate integral. # If entrance_rate_integral is None, numerical integration will # be used. self.entrance_rate_integral = entrance_rate_integral self.entrance_rate_integral_inverse = entrance_rate_integral_inverse self.pedestrians = SortedKeyList(key=attrgetter('start_time')) self._counts = SortedDict() self._counts[self.start_time] = 0 self._counts_are_correct = True self.refresh_pedestrians() def _distance(self, a, b): """signed distance of a relative to b""" return circular_diff(a % self.dist_around, b % self.dist_around, self.dist_around) def _distance_from(self, b): """returns a function that returns the signed sitance from b""" return lambda a: self._distance(a, b) def _abs_distance_from(self, b): """returns a function that returns the distance from b""" return lambda a: abs(self._distance(a, b)) def _closest_exit(self, dist): """Returns the closest number to dist that is equivalent mod dist_around to an element of entrances""" closest_exit = min(self.entrances, key=self._abs_distance_from(dist)) diff = self._distance(closest_exit, dist) corrected_dist = dist + diff return corrected_dist def refresh_pedestrians(self): """Refreshes the pedestrians in the simulation to random ones""" self.clear_pedestrians() start_times = list( random_times(self.start_time, self.end_time, self.entrance_rate, self.entrance_rate_integral, self.entrance_rate_integral_inverse)) n_pedestrians = len(start_times) entrances = random.choices(population=self.entrances, weights=self.entrance_weights, k=n_pedestrians) velocities, distances = self.rand_velocities_and_distances( n_pedestrians).T def pedestrians_generator(): for start_time, entrance, velocity, dist in zip( start_times, entrances, velocities, distances): assert dist > 0 if self._snap_exit: original_exit = entrance + dist * sign(velocity) corrected_exit = self._closest_exit(original_exit) corrected_dist = abs(corrected_exit - entrance) if math.isclose(corrected_dist, 0, abs_tol=1e-10): corrected_dist = self.dist_around else: corrected_dist = dist yield FreshPondPedestrian(self.dist_around, entrance, corrected_dist, start_time, velocity) self.add_pedestrians(pedestrians_generator()) def clear_pedestrians(self): """Removes all pedestrains in the simulation""" self.pedestrians.clear() self._reset_counts() self._counts_are_correct = True def add_pedestrians(self, pedestrians): """Adds all the given pedestrians to the simulation""" def checked_pedestrians(): for p in pedestrians: self._assert_pedestrian_in_range(p) yield p initial_num_pedestrians = self.num_pedestrians() self.pedestrians.update(checked_pedestrians()) final_num_pedestrians = self.num_pedestrians() if final_num_pedestrians > initial_num_pedestrians: self._counts_are_correct = False else: assert final_num_pedestrians == initial_num_pedestrians def _assert_pedestrian_in_range(self, p): """Makes sure the pedestrian's start time is in the simulation's time interval""" if not (self.start_time <= p.start_time < self.end_time): raise ValueError( "Pedestrian start time is not in range [start_time, end_time)") def add_pedestrian(self, p): """Adds a new pedestrian to the simulation""" self._assert_pedestrian_in_range(p) self.pedestrians.add(p) # Update counts only when counts are correct if self._counts_are_correct: # add a new breakpoint at the pedestrian's start time if it not there self._counts[p.start_time] = self.n_people(p.start_time) # add a new breakpoint at the pedestrian's end time if it not there self._counts[p.end_time] = self.n_people(p.end_time) # increment all the counts in the pedestrian's interval of time # inclusive on the left, exclusive on the right # If it were inclusive on the right, then the count would be one more # than it should be in the period after end_time and before the next # breakpoint after end_time for t in self._counts.irange(p.start_time, p.end_time, inclusive=(True, False)): self._counts[t] += 1 def _reset_counts(self): """Clears _counts and sets count at start_time to 0""" self._counts.clear() self._counts[self.start_time] = 0 def _recompute_counts(self): """Store how many people there are whenever someone enters or exits so the number of people at a given time can be found quickly later""" # print("Recomputing counts") self._reset_counts() if self.num_pedestrians() == 0: return # pedestrians are already sorted by start time start_times = [p.start_time for p in self.pedestrians] end_times = sorted([p.end_time for p in self.pedestrians]) n = len(start_times) curr_count = 0 # current number of people start_times_index = 0 end_times_index = 0 starts_done = False # whether all the start times have been added ends_done = False # whether all the end times have been added while not (starts_done and ends_done): # determine whether a start time or an end time should be added next # store this in the variable take_start which is true if a start # time should be added next if starts_done: # already added all the start times; add an end time take_start = False elif ends_done: # already added all the end times; add a start time take_start = True else: # didn't add all the end times nor all the start times # add the time that is earliest next_start_time = start_times[start_times_index] next_end_time = end_times[end_times_index] take_start = next_start_time < next_end_time if take_start: # add next start curr_count += 1 start_time = start_times[start_times_index] self._counts[start_time] = curr_count start_times_index += 1 if start_times_index == n: starts_done = True else: # add next end curr_count -= 1 end_time = end_times[end_times_index] self._counts[end_time] = curr_count end_times_index += 1 if end_times_index == n: ends_done = True def n_unique_people_saw(self, p): """Returns the number of unique people that a pedestrian sees""" n = 0 for q in self.pedestrians: if p.intersects(q): n += 1 return n def n_people_saw(self, p): """Returns the number of times a pedestrian sees someone""" n = 0 for q in self.pedestrians: if p.end_time > q.start_time and p.start_time < q.end_time: n += p.n_intersections(q) return n def intersection_directions(self, p): """Returns the number of people seen going in the same direction and the number of people seen going in the opposite direction by p as a tuple""" n_same, n_diff = 0, 0 for q in self.pedestrians: if p.end_time > q.start_time and p.start_time < q.end_time: d = q.intersection_direction(p) if d == 1: n_same += 1 elif d == -1: n_diff += 1 return n_same, n_diff def intersection_directions_total(self, p): n_same, n_diff = 0, 0 for q in self.pedestrians: if p.end_time > q.start_time and p.start_time < q.end_time: i = p.total_intersection_direction(q) if i < 0: n_diff += -i elif i > 0: n_same += i return n_same, n_diff def n_people(self, t): """Returns the number of people at a given time""" if not self._counts_are_correct: self._recompute_counts() self._counts_are_correct = True if t in self._counts: return self._counts[t] elif t < self.start_time: return 0 else: index = self._counts.bisect_left(t) return self._counts.values()[index - 1] def num_pedestrians(self): """Returns the total number of pedestrians in the simulation""" return len(self.pedestrians) def get_pedestrians_in_interval(self, start, stop): """Returns a list of all the pedestrians who entered in the interval [start, stop]""" return list(self.pedestrians.irange_key(start, stop)) def num_entrances_in_interval(self, start, stop): """Returns the number of pedestrians who entered in the given interval of time [start, stop]""" return len(self.get_pedestrians_in_interval(start, stop)) def get_enter_and_exit_times_in_interval(self, start, stop): """Returns the entrance and exit times in a given time interval as a tuple of lists (entrance_times, exit_times).""" start_times = [] end_times = [] for p in self.pedestrians: if start <= p.start_time <= stop: start_times.append(p.start_time) if start <= p.end_time <= stop: end_times.append(p.end_time) return start_times, end_times def get_pedestrians_at_time(self, t): """Returns a list of all the pedestrians who were there at time t""" # get all pedestrians who entered at or before time t entered_before_t = self.pedestrians.irange_key( min_key=None, max_key=t, inclusive=(True, True)) # Of those, return return the ones who exited after time t return [p for p in entered_before_t if p.end_time > t]
def _dump_memo2_chart( difficulty: str, chart: Chart, metadata: Metadata, timing: Timing, circle_free: bool = False, ) -> StringIO: _raise_if_unfit_for_memo2(chart, timing, circle_free) def make_section(b: BeatsTime) -> Memo2Section: return Memo2Section() sections = SortedDefaultDict(make_section) timing_events = sorted(timing.events, key=lambda e: e.time) notes = SortedKeyList(set(chart.notes), key=lambda n: n.time) for note in chart.notes: if isinstance(note, LongNote): notes.add(LongNoteEnd(note.time + note.duration, note.position)) all_events = SortedKeyList(timing_events + notes, key=lambda n: n.time) last_event = all_events[-1] last_measure = last_event.time // 4 for i in range(last_measure + 1): beat = BeatsTime(4) * i sections.add_key(beat) # Timing events sections[BeatsTime(0)].events.append( StopEvent(BeatsTime(0), timing.beat_zero_offset) ) for event in timing_events: section_beat = event.time - (event.time % 4) sections[section_beat].events.append(event) # Fill sections with notes for key, next_key in windowed(chain(sections.keys(), [None]), 2): assert key is not None sections[key].notes = list( notes.irange_key(min_key=key, max_key=next_key, inclusive=(True, False)) ) # Actual output to file file = StringIO() file.write(f"// Converted using jubeatools {__version__}\n") file.write(f"// https://github.com/Stepland/jubeatools\n\n") # Header file.write(dump_command("lev", Decimal(chart.level)) + "\n") file.write(dump_command("dif", DIFFICULTY_NUMBER.get(difficulty, 1)) + "\n") if metadata.audio is not None: file.write(dump_command("m", metadata.audio) + "\n") if metadata.title is not None: file.write(dump_command("title", metadata.title) + "\n") if metadata.artist is not None: file.write(dump_command("artist", metadata.artist) + "\n") if metadata.cover is not None: file.write(dump_command("jacket", metadata.cover) + "\n") if metadata.preview is not None: file.write(dump_command("prevpos", int(metadata.preview.start * 1000)) + "\n") if any(isinstance(note, LongNote) for note in chart.notes): file.write(dump_command("holdbyarrow", 1) + "\n") if circle_free: file.write(dump_command("circlefree", 1) + "\n") file.write(dump_command("memo2") + "\n") file.write("\n") # Notes file.write( "\n\n".join(section.render(circle_free) for _, section in sections.items()) ) return file
def create_sections_from_chart( section_factory: Callable[[BeatsTime], JubeatAnalyserDumpedSection], chart: Chart, difficulty: str, timing: Timing, metadata: Metadata, circle_free: bool, ) -> Mapping[BeatsTime, JubeatAnalyserDumpedSection]: sections = SortedDefaultDict(section_factory) timing_events = sorted(timing.events, key=lambda e: e.time) notes = SortedKeyList(set(chart.notes), key=lambda n: n.time) for note in chart.notes: if isinstance(note, LongNote): notes.add(LongNoteEnd(note.time + note.duration, note.position)) all_events = SortedKeyList(timing_events + notes, key=lambda n: n.time) last_event = all_events[-1] last_measure = last_event.time // 4 for i in range(last_measure + 1): beat = BeatsTime(4) * i sections.add_key(beat) header = sections[BeatsTime(0)].commands header["o"] = int(timing.beat_zero_offset * 1000) header["lev"] = Decimal(chart.level) header["dif"] = DIFFICULTY_NUMBER.get(difficulty, 3) if metadata.audio is not None: header["m"] = metadata.audio if metadata.title is not None: header["title"] = metadata.title if metadata.artist is not None: header["artist"] = metadata.artist if metadata.cover is not None: header["jacket"] = metadata.cover if metadata.preview is not None: header["prevpos"] = int(metadata.preview.start * 1000) if any(isinstance(note, LongNote) for note in chart.notes): header["holdbyarrow"] = 1 if circle_free: header["circlefree"] = 1 # Potentially create sub-sections for bpm changes for event in timing_events: sections[event.time].commands["t"] = event.BPM # First, Set every single b=… value for key, next_key in windowed(chain(sections.keys(), [None]), 2): if key is None: continue elif next_key is None: length = BeatsTime(4) else: length = next_key - key sections[key].commands["b"] = length sections[key].length = length # Then, trim all the redundant b=… last_b: Union[int, Fraction, Decimal] = 4 for section in sections.values(): current_b = section.commands["b"] if current_b == last_b: del section.commands["b"] else: last_b = current_b # Fill sections with notes for key, next_key in windowed(chain(sections.keys(), [None]), 2): assert key is not None sections[key].notes = list( notes.irange_key(min_key=key, max_key=next_key, inclusive=(True, False))) return sections
class Market: def __init__(self): # Implimentation note: Orders are kept in order, such that the most # competative orders are the first in the list, and older orders have # priority over newer ones. self.sell_orders = SortedKeyList( key=lambda order: (order.offer_price, order.order_number)) self.buy_orders = SortedKeyList( key=lambda order: (-order.offer_price, order.order_number)) self.order_number = 1 self._todays_volume = 0 self._daily_volumes = deque(maxlen=30) self._todays_high = None self._daily_highs = deque(maxlen=30) self._todays_low = None self._daily_lows = deque(maxlen=30) self._last_price = 0 self._daily_closing_price = deque(maxlen=30) def has_buy_orders(self): return len(self.buy_orders) > 0 def has_sell_orders(self): return len(self.sell_orders) > 0 def get_30d_avg_volume(self): if len(self._daily_volumes) == 0: return 0 return mean(self._daily_volumes) def get_30d_avg_price(self): if len(self._daily_closing_price) == 0: return 0 return mean(self._daily_closing_price) def get_30d_sigma_price(self): if len(self._daily_closing_price) == 0: return 0 return mean(self._daily_closing_price) def last_session_open(self): if len(self._daily_closing_price) < 2: return 0 return self._daily_closing_price[-2] def last_session_close(self): if len(self._daily_closing_price) < 1: return 0 return self._daily_closing_price[-1] def last_session_volume(self): if len(self._daily_volumes) < 1: return 0 return self._daily_volumes[-1] def last_session_high(self): if len(self._daily_highs) < 1: return 0 return self._daily_highs[-1] def last_session_low(self): if len(self._daily_lows) < 1: return 0 return self._daily_lows[-1] def best_buy_orders(self): """ Get all buy orders that share the most competative (highest) offer price. Orders are returned according to the same sorting as the larger order list. """ best_buy_order_price = self.buy_orders[0].offer_price return self.buy_orders.irange_key( (-best_buy_order_price, 0), (-best_buy_order_price, self.order_number)) def best_sell_orders(self): """ Get all sell orders that share the most competative (lowest) offer price Orders are returned according to the same sorting as the larger order list. """ best_sell_order_price = self.sell_orders[0].offer_price return self.sell_orders.irange_key( (best_sell_order_price, 0), (best_sell_order_price, self.order_number)) def lowest_sell_offer(self): """ Get the most competitive sell price """ return self.sell_orders[0].offer_price def highest_buy_offer(self): """ Get the most competitive buy price """ return self.buy_orders[0].offer_price def place_buy_order(self, offer_price: float, quantity: int, callback: OrderCallback) -> BuyOrder: """ Places an order, which is returned (unfilled) to the caller. Upon fulfilment, the person holding the order is called back with `.fill_order(order, amount_filled)`. This market is not doing escarow, and it is assumed that the person has kept the needed money in hand to be removed now in exchange for goods. """ assert quantity > 0 assert offer_price > 0 order = BuyOrder(self.order_number, offer_price, quantity, callback) self.order_number += 1 self.buy_orders.add(order) return order def place_sell_order(self, offer_price: float, quantity: int, callback: OrderCallback) -> SellOrder: """ Places an order, which is returned (unfilled) to the caller. Upon fulfilment, the person holding the order is called back with `.fill_order(order, amount_filled)`. This market is not doing escarow, and it is assumed that the person has kept the needed goods in hand to be removed now in exchange for money. """ assert quantity > 0 assert offer_price > 0 order = SellOrder(self.order_number, offer_price, quantity, callback) self.order_number += 1 self.sell_orders.add(order) return order def cancel_buy_order(self, order: BuyOrder): """ Cancels a buy order. If the order is not in the market, raises a ValueError """ self.buy_orders.remove(order) def cancel_sell_order(self, order: SellOrder): """ Cancels a sell order. If the order is not in the market, raises a ValueError """ self.sell_orders.remove(order) def _resolve_orders(self, orders, num_resolved): """ Resolve a number of orders as much as possible. In the trivial case, all orders are totally filled. In more complicated resolutions, orders are resolved oldest first, leaving some orders unfilled or partially filled. returns a list of orders that have been wholy or partially filled """ remaining = num_resolved modified_orders = [] for order in orders: to_fill = min(order.quantity_unfilled(), remaining) if to_fill > 0: remaining -= to_fill order._fill(to_fill) modified_orders.append((order, to_fill)) if remaining == 0: break return modified_orders def execute_orders(self): while (len(self.buy_orders) > 0 and len(self.sell_orders) > 0 and self.highest_buy_offer() >= self.lowest_sell_offer()): best_buy_offers = list(self.best_buy_orders()) best_sell_offers = list(self.best_sell_orders()) buy_quantity = count_quantity(best_buy_offers) sell_quantity = count_quantity(best_sell_offers) quantity_resolved = min(buy_quantity, sell_quantity) assert quantity_resolved > 0 self._todays_volume += quantity_resolved strike_price = best_buy_offers[0].offer_price self._last_price = strike_price if self._todays_high == None: self._todays_high = strike_price else: self._todays_high = max(self._todays_high, strike_price) if self._todays_low == None: self._todays_low = strike_price else: self._todays_low = min(self._todays_low, strike_price) executed_buy_orders = self._resolve_orders(best_buy_offers, quantity_resolved) executed_sell_orders = self._resolve_orders( best_sell_offers, quantity_resolved) assert len(executed_buy_orders) > 0 assert len(executed_sell_orders) > 0 for order, num_filled in executed_buy_orders: if order.is_filled(): self.buy_orders.remove(order) for order, num_filled in executed_sell_orders: if order.is_filled(): self.sell_orders.remove(order) # Finally, inform the actors that the orders are executed for order, num_filled in executed_buy_orders: order.callback(order, num_filled) for order, num_filled in executed_sell_orders: order.callback(order, num_filled) def tick(self): self._daily_closing_price.append(self._last_price) self._daily_volumes.append(self._todays_volume) self._todays_volume = 0 self._daily_highs.append(self._todays_high) self._todays_high = None self._daily_lows.append(self._todays_low) self._todays_low = None
class SortedIntvls: """ """ def __init__(self): # we sort by increasing start offset then increasing annotation id for this self._by_start = SortedKeyList(key=lambda x: (x[0], x[2])) # for this we sort by end offset only self._by_end = SortedKeyList(key=lambda x: x[1]) def add(self, start, end, data): """ Adds an interval. """ self._by_start.add((start, end, data)) self._by_end.add((start, end, data)) def update(self, tupleiterable): """ Updates from an iterable of intervals. """ self._by_start.update(tupleiterable) self._by_end.update(tupleiterable) def remove(self, start, end, data): """ Removes an interval, exception if the interval does not exist. """ self._by_start.remove((start, end, data)) self._by_end.remove((start, end, data)) def discard(self, start, end, data): """ Removes and interval, do nothing if the interval does not exist. """ self._by_start.discard((start, end, data)) self._by_end.discard((start, end, data)) def __len__(self): """ Returns the number of intervals. """ return len(self._by_start) def starting_at(self, offset): """ Returns an iterable of (start, end, data) tuples where start==offset """ return self._by_start.irange_key(min_key=(offset, 0), max_key=(offset, sys.maxsize)) def ending_at(self, offset): """ Returns an iterable of (start, end, data) tuples where end==offset """ return self._by_end.irange_key(min_key=offset, max_key=offset) def at(self, start, end): """ Returns an iterable of tuples where start==start and end==end """ for intvl in self._by_start.irange_key(min_key=(start, 0), max_key=(start, sys.maxsize)): if intvl[1] == end: yield intvl def within(self, start, end): """ Returns intervals which are fully contained within start...end """ # get all the intervals that start within the range, then keep those which also end within the range for intvl in self._by_start.irange_key(min_key=(start, 0), max_key=(end, sys.maxsize)): if intvl[1] <= end: yield intvl def starting_from(self, offset): """ Returns intervals that start at or after offset. """ return self._by_start.irange_key(min_key=(offset, 0)) def starting_before(self, offset): """ Returns intervals that start before offset. """ return self._by_start.irange_key(max_key=(offset - 1, sys.maxsize)) def ending_to(self, offset): """ Returns intervals that end before or at the given end offset. """ return self._by_end.irange_key(max_key=offset) def ending_after(self, offset): """ Returns intervals the end after the given offset. """ return self._by_end.irange_key(min_key=offset + 1) def covering(self, start, end): """ Returns intervals that contain the given range. """ # All intervals that start at or before the start and end at or after the end offset # we do this by first getting the intervals the start before or atthe start # then filtering by end for intvl in self._by_start.irange_key(max_key=(start, sys.maxsize)): if intvl[1] >= end: yield intvl def overlapping(self, start, end): """ Returns intervals that overlap with the given range. """ # All intervals where the start or end offset lies within the given range. # This excludes the ones where the end offset is before the start or # where the start offset is after the end of the range. # Here we do this by looking at all intervals where the start offset is before the # end of the range. This still includes those which also end before the start of the range # so we check in addition that the end is larger than the start of the range. for intvl in self._by_start.irange_key(max_key=(end - 1, sys.maxsize)): if intvl[1] > start + 1: yield intvl def firsts(self): """ Yields all intervals which start at the smallest known offset. """ laststart = None # logger.info("DEBUG: set laststart to None") for intvl in self._by_start.irange_key(): # logger.info("DEBUG: checking interval {}".format(intvl)) if laststart is None: laststart = intvl[0] # logger.info("DEBUG: setting laststart to {} and yielding {}".format(intvl[0], intvl)) yield intvl elif intvl[0] == laststart: # logger.info("DEBUG: yielding {}".format(intvl)) yield intvl else: # logger.info("DEBUG: returning since we got {}".format(intvl)) return def lasts(self): """ Yields all intervals which start at the last known start offset. """ laststart = None for intvl in reversed(self._by_start): if laststart is None: laststart = intvl[0] yield intvl elif intvl[0] == laststart: yield intvl else: return def min_start(self): """ Returns the smallest known start offset. """ return self._by_start[0][0] def max_end(self): """ Returns the biggest known end offset. """ return self._by_end[-1][1] def irange(self, minoff=None, maxoff=None, reverse=False, inclusive=(True, True)): """ Yields an iterator of intervals with a start offset between minoff and maxoff, inclusive. Args: minoff: minimum offset, default None indicates any maxoff: maximum offset, default None indicates any reverse: if `True` yield in reverse order inclusive: if the minoff and maxoff values should be inclusive, default is (True,True) Returns: """ return self._by_start.irange_key(min_key=minoff, max_key=maxoff, reverse=reverse, inclusive=inclusive) def __repr__(self): return "SortedIntvls({},{})".format(self._by_start, self._by_end)