{$MODE OBJFPC} { -*- delphi -*- } {$INCLUDE settings.inc} unit schedule; interface uses sysutils, time, plasticarrays; type PScheduleEntry = ^TScheduleEntry; TScheduleEntry = record TimeOrigin: Int32; Period: Int32; Delta: Int32; Reference: Pointer; end; TSchedule = array of TScheduleEntry; TScheduleEntryUtils = record class function Equals(const A, B: TScheduleEntry): Boolean; static; inline; class function LessThan(const A, B: TScheduleEntry): Boolean; static; inline; class function GreaterThan(const A, B: TScheduleEntry): Boolean; static; inline; class function Compare(const A, B: TScheduleEntry): Int64; static; inline; end; TScheduleBuilder = record private FRecords: specialize PlasticArray; public procedure Inc(TimeOrigin: Int32; Rate: TIterationsRate; Delta: Int32; Reference: Pointer); procedure Inc(TimeOrigin: Int32; Rate: TQuantityRate; Reference: Pointer); procedure Dec(TimeOrigin: Int32; Rate: TIterationsRate; Delta: Int32; Reference: Pointer); procedure Dec(TimeOrigin: Int32; Rate: TQuantityRate; Reference: Pointer); function Compile(): TSchedule; end; function FindScheduleEnd(Schedule: TSchedule; MinTotal, MaxTotal, MaxDuration: Int64; out Entry: PScheduleEntry): Int64; implementation class function TScheduleEntryUtils.Equals(const A, B: TScheduleEntry): Boolean; static; inline; begin Assert(False, 'why did we use this'); Result := (A.TimeOrigin = B.TimeOrigin) and (A.Period = B.Period) and (A.Delta = B.Delta) and (A.Reference = B.Reference); end; class function TScheduleEntryUtils.LessThan(const A, B: TScheduleEntry): Boolean; static; inline; begin if (A.TimeOrigin + A.Period < B.TimeOrigin + B.Period) then Result := True else if (A.TimeOrigin + A.Period > B.TimeOrigin + B.Period) then Result := False else if (A.Delta < B.Delta) then Result := True else if (A.Delta > B.Delta) then Result := False else if (A.Reference < B.Reference) then Result := True else Result := False; end; class function TScheduleEntryUtils.GreaterThan(const A, B: TScheduleEntry): Boolean; static; inline; begin if (A.TimeOrigin + A.Period > B.TimeOrigin + B.Period) then Result := True else if (A.TimeOrigin + A.Period < B.TimeOrigin + B.Period) then Result := False else if (A.Delta > B.Delta) then Result := True else if (A.Delta < B.Delta) then Result := False else if (A.Reference > B.Reference) then Result := True else Result := False; end; class function TScheduleEntryUtils.Compare(const A, B: TScheduleEntry): Int64; static; inline; begin Result := A.TimeOrigin + A.Period - B.TimeOrigin + B.Period; if (Result = 0) then begin Result := A.Delta - B.Delta; if (Result = 0) then Result := A.Reference - B.Reference; end; end; procedure TScheduleBuilder.Inc(TimeOrigin: Int32; Rate: TIterationsRate; Delta: Int32; Reference: Pointer); var Interval: Int64; Entry: TScheduleEntry; begin Interval := Rate.ComputePeriod().AsInt64; Assert(Interval <= High(Entry.Period)); Assert(Interval > 0); if (Interval > High(Entry.Period)) then Interval := High(Entry.Period); // saturate Entry.TimeOrigin := TimeOrigin; Entry.Period := Int32(Interval); Entry.Delta := Delta; FRecords.Push(Entry); end; procedure TScheduleBuilder.Inc(TimeOrigin: Int32; Rate: TQuantityRate; Reference: Pointer); var Interval: Int64; Entry: TScheduleEntry; begin Interval := Rate.ComputeMinCycleTime().AsInt64; Assert(Interval <= High(Entry.Period)); if (Interval > High(Entry.Period)) then Interval := High(Entry.Period); // saturate Assert(Interval <> 0); Entry.TimeOrigin := TimeOrigin; Entry.Period := Int32(Interval); Entry.Delta := 1; FRecords.Push(Entry); end; procedure TScheduleBuilder.Dec(TimeOrigin: Int32; Rate: TIterationsRate; Delta: Int32; Reference: Pointer); begin Assert(Delta > 0); Inc(TimeOrigin, Rate, -Delta, Reference); // $R- end; procedure TScheduleBuilder.Dec(TimeOrigin: Int32; Rate: TQuantityRate; Reference: Pointer); var Interval: Int64; Entry: TScheduleEntry; begin Interval := Rate.ComputeMinCycleTime().AsInt64; Assert(Interval <= High(Entry.Period)); if (Interval > High(Entry.Period)) then Interval := High(Entry.Period); // saturate Assert(Interval <> 0); Entry.TimeOrigin := TimeOrigin; Entry.Period := Int32(Interval); Entry.Delta := -1; FRecords.Push(Entry); end; function TScheduleBuilder.Compile(): TSchedule; begin if (FRecords.Length > 0) then begin if (FRecords.Length > 1) then FRecords.Sort(); Result := FRecords.Distill(); end; end; function FindScheduleEnd(Schedule: TSchedule; MinTotal, MaxTotal, MaxDuration: Int64; out Entry: PScheduleEntry): Int64; begin end; {$PUSH} {$OVERFLOWCHECKS OFF} {$RANGECHECKS OFF} { The schedule.impl.inc file is from ChatGPT using the following prompt: The user has the following pascal data structure definitions: ``` type PScheduleEntry = ^TScheduleEntry; TScheduleEntry = record TimeOrigin: Int32; Period: Int32; Delta: Int32; Reference: Pointer; end; TSchedule = array of TScheduleEntry; ``` The TSchedule data structure represents a running total being updated at intervals. Each entry in the array represents a recurring event that happens every Period time units, starting after TimeOrigin+Period time units, where the running total is increased by Delta. TimeOrigin is greater than -Period and less than or equal to zero. Period is always greater than zero. The TSchedule is sorted by TimeOrigin+Period (i.e. entries are in the order that they will first affect the running total). A TSchedule array will usually have less than 1000 entries. There exists the following function: ``` function FindScheduleEnd(Schedule: TSchedule; MinTotal, MaxTotal, MaxDuration: Int64; out Entry: PScheduleEntry): Int64; ``` The function returns the time step where the running total goes above the MaxTotal argument or below the MinTotal argument after applying all entries for that timestep, if any; if the data structure oscillates stably within that range, or does not pass either threshold before MaxDuration, it will return MaxDuration. If the running total goes above MaxTotal, the Entry argument will be set to a pointer to the TScheduleEntry that had the most positive Delta at that time step. If the running total goes below MinTotal, the Entry argument will be set to a pointer to the TScheduleEntry that had the most negative Delta at that time step. If the situation is stable (at least up to MaxDuration), Entry will be nil. In the event of ties, the order of entries in the Schedule will be used as a tiebreaker. A complete cycle of the Schedule may take longer than MaxDuration, or may take less. A possible approach is to maintain a heap that is initialized with the schedule, using as the key for each entry its next iteration time. The first item on the heap is pulled, the running total is updated, and the item is reinserted into the heap with the next iteration time. If the next iteration time of the first item in the heap is after MaxDuration, the function terminates with that time and Entry set to nil. If the running total exceeds a threshold, the function returns appropriately. The function should detect stable situations (where it can tell the running total will not exceed the threshold before the MaxDuration), and return early appropriately. Implement this function. } {$INCLUDE schedule.impl.inc} {$IFDEF TESTS} { The schedule.tests.inc file is from Claude using the following prompt: The user has the following pascal data structure definitions: ``` type PScheduleEntry = ^TScheduleEntry; TScheduleEntry = record TimeOrigin: Int32; Period: Int32; Delta: Int32; Reference: Pointer; end; TSchedule = array of TScheduleEntry; ``` The TSchedule data structure represents a running total being updated at intervals. Each entry in the array represents a recurring event that happens every Period time units, starting after TimeOrigin+Period time units, where the running total is increased by Delta. TimeOrigin is greater than -Period and less than or equal to zero. Period is always greater than zero. The TSchedule is sorted by TimeOrigin+Period (i.e. entries are in the order that they will first affect the running total). A TSchedule array will usually have less than 1000 entries. There exists the following function: ``` function FindScheduleEnd(Schedule: TSchedule; MinTotal, MaxTotal, MaxDuration: Int64; out Entry: PScheduleEntry): Int64; ``` The function returns the time step where the running total goes above the MaxTotal argument or below the MinTotal argument after applying all entries for that timestep, if any; if the data structure oscillates stably within that range, or does not pass either threshold before MaxDuration, it will return MaxDuration. If the running total goes above MaxTotal, the Entry argument will be set to a pointer to the TScheduleEntry that had the most positive Delta at that time step. If the running total goes below MinTotal, the Entry argument will be set to a pointer to the TScheduleEntry that had the most negative Delta at that time step. If the situation is stable (at least up to MaxDuration), Entry will be nil. In the event of ties, the order of entries in the Schedule will be used as a tiebreaker. A complete cycle of the Schedule may take longer than MaxDuration, or may take less. There exists the following procedure, which runs a comprehensive set of test cases for FindScheduleEnd, including cases where there are many entries with varied values in the schedule: ``` procedure TestFindScheduleEnd(); ``` This procedure writes no output except if a test fails, in which case it prints detailed diagnostic information and then calls Halt(1). Implement this procedure. } {$INCLUDE schedule.tests.inc} initialization TestFindScheduleEnd(); {$ENDIF} {$POP} end.