@@ -2,7 +2,9 @@ import { adapter, EventBuilder } from 'test/utils/scheduler';
22import { renderHook } from '@mui/internal-test-utils' ;
33import type { SchedulerProcessedEvent } from '@mui/x-scheduler-internals/models' ;
44import { getOccurrencesFromEvents } from '@mui/x-scheduler-internals/internals' ;
5+ import { sortEventOccurrences } from '@mui/x-scheduler-internals/sort-event-occurrences' ;
56import {
7+ computeOccurrencesWithTimelinePosition ,
68 computeOccurrencesFirstIndexLookup ,
79 useEventOccurrencesWithTimelinePosition ,
810} from './useEventOccurrencesWithTimelinePosition' ;
@@ -237,12 +239,145 @@ describe('useDayListEventOccurrencesWithPosition', () => {
237239 visibleResources : { } ,
238240 recurringEventsPlugin : null ,
239241 } ) ;
240- const lookup = computeOccurrencesFirstIndexLookup ( adapter , occurrences ) ;
242+ const lookup = computeOccurrencesFirstIndexLookup ( occurrences ) ;
241243
242244 expect ( Object . keys ( lookup ) ) . to . have . length ( result . occurrences . length ) ;
243245 for ( const occurrence of result . occurrences ) {
244246 expect ( lookup [ occurrence . key ] ) . to . equal ( occurrence . position . firstIndex ) ;
245247 }
246248 } ) ;
247249 } ) ;
250+
251+ it ( 'should update the available span when a freed lane is reused' , ( ) => {
252+ const occurrences = [
253+ EventBuilder . new ( ) . id ( 'A' ) . singleDay ( '2024-01-15T10:00:00Z' , 30 ) . toOccurrence ( ) ,
254+ EventBuilder . new ( ) . id ( 'B' ) . singleDay ( '2024-01-15T10:01:00Z' , 39 ) . toOccurrence ( ) ,
255+ EventBuilder . new ( ) . id ( 'C' ) . singleDay ( '2024-01-15T10:10:00Z' , 120 ) . toOccurrence ( ) ,
256+ EventBuilder . new ( ) . id ( 'D' ) . singleDay ( '2024-01-15T10:50:00Z' , 60 ) . toOccurrence ( ) ,
257+ EventBuilder . new ( ) . id ( 'E' ) . singleDay ( '2024-01-15T11:00:00Z' , 30 ) . toOccurrence ( ) ,
258+ ] ;
259+
260+ const resultBeforeReuse = computeOccurrencesWithTimelinePosition (
261+ occurrences . slice ( 0 , - 1 ) ,
262+ Infinity ,
263+ ) ;
264+ const result = computeOccurrencesWithTimelinePosition ( occurrences , Infinity ) ;
265+
266+ expect ( resultBeforeReuse . occurrences [ 3 ] . position ) . to . deep . equal ( {
267+ firstIndex : 1 ,
268+ lastIndex : 2 ,
269+ } ) ;
270+ expect ( result . maxIndex ) . to . equal ( 3 ) ;
271+ expect ( result . occurrences . map ( ( { id, position } ) => ( { id, ...position } ) ) ) . to . deep . equal ( [
272+ { id : 'A' , firstIndex : 1 , lastIndex : 1 } ,
273+ { id : 'B' , firstIndex : 2 , lastIndex : 2 } ,
274+ { id : 'C' , firstIndex : 3 , lastIndex : 3 } ,
275+ { id : 'D' , firstIndex : 1 , lastIndex : 1 } ,
276+ { id : 'E' , firstIndex : 2 , lastIndex : 2 } ,
277+ ] ) ;
278+ } ) ;
279+
280+ it ( 'should match the quadratic layout for varied event distributions' , ( ) => {
281+ let seed = 239 ;
282+ const random = ( ) => {
283+ seed = ( seed * 16807 ) % 2147483647 ;
284+ return ( seed - 1 ) / 2147483646 ;
285+ } ;
286+
287+ for ( let iteration = 0 ; iteration < 100 ; iteration += 1 ) {
288+ const occurrenceCount = 1 + Math . floor ( random ( ) * 40 ) ;
289+ const occurrences = Array . from ( { length : occurrenceCount } , ( _ , index ) => {
290+ const startMinutes = Math . floor ( random ( ) * 12 ) * 30 ;
291+ const durationMinutes = Math . floor ( random ( ) * 9 ) * 15 ;
292+ const start = adapter . addMinutes ( collectionStart , startMinutes ) . toISOString ( ) ;
293+ return EventBuilder . new ( )
294+ . id ( `${ iteration } -${ index } ` )
295+ . singleDay ( start , durationMinutes )
296+ . toOccurrence ( ) ;
297+ } ) ;
298+ const maxSpan = [ 1 , 2 , 4 , Infinity ] [ iteration % 4 ] ;
299+
300+ const result = computeOccurrencesWithTimelinePosition ( occurrences , maxSpan ) ;
301+ const expected = computeLayoutQuadratically ( occurrences , maxSpan ) ;
302+
303+ expect ( result . occurrences . map ( ( { id, position } ) => ( { id, ...position } ) ) ) . to . deep . equal (
304+ expected . occurrences ,
305+ ) ;
306+ expect ( result . maxIndex ) . to . equal ( expected . maxIndex ) ;
307+ }
308+ } ) ;
309+
310+ it ( 'should position a dense set of occurrences' , ( ) => {
311+ const occurrences = Array . from ( { length : 1_000 } , ( _ , index ) =>
312+ EventBuilder . new ( ) . id ( index ) . singleDay ( '2024-01-15T10:00:00Z' , 120 ) . toOccurrence ( ) ,
313+ ) ;
314+
315+ const result = computeOccurrencesWithTimelinePosition ( occurrences , Infinity ) ;
316+
317+ expect ( result . maxIndex ) . to . equal ( 1_000 ) ;
318+ expect ( result . occurrences [ 0 ] . position ) . to . deep . equal ( { firstIndex : 1 , lastIndex : 1 } ) ;
319+ expect ( result . occurrences [ 999 ] . position ) . to . deep . equal ( {
320+ firstIndex : 1_000 ,
321+ lastIndex : 1_000 ,
322+ } ) ;
323+ } ) ;
248324} ) ;
325+
326+ function computeLayoutQuadratically (
327+ occurrences : ReturnType < EventBuilder [ 'toOccurrence' ] > [ ] ,
328+ maxSpan : number ,
329+ ) {
330+ const sortedOccurrences = sortEventOccurrences ( occurrences ) ;
331+ const firstIndexLookup : Record < string , number > = { } ;
332+ let maxIndex = 1 ;
333+
334+ for ( let index = 0 ; index < sortedOccurrences . length ; index += 1 ) {
335+ const occurrence = sortedOccurrences [ index ] ;
336+ const usedIndexes = new Set < number > ( ) ;
337+ for ( let previousIndex = 0 ; previousIndex < index ; previousIndex += 1 ) {
338+ const previousOccurrence = sortedOccurrences [ previousIndex ] ;
339+ if (
340+ previousOccurrence . displayTimezone . end . timestamp >
341+ occurrence . displayTimezone . start . timestamp
342+ ) {
343+ usedIndexes . add ( firstIndexLookup [ previousOccurrence . key ] ) ;
344+ }
345+ }
346+
347+ let firstIndex = 1 ;
348+ while ( usedIndexes . has ( firstIndex ) ) {
349+ firstIndex += 1 ;
350+ }
351+ firstIndexLookup [ occurrence . key ] = firstIndex ;
352+ maxIndex = Math . max ( maxIndex , firstIndex ) ;
353+ }
354+
355+ return {
356+ occurrences : sortedOccurrences . map ( ( occurrence , index ) => {
357+ const firstIndex = firstIndexLookup [ occurrence . key ] ;
358+ const usedIndexes = new Set (
359+ sortedOccurrences
360+ . filter ( ( otherOccurrence , otherIndex ) =>
361+ otherIndex < index
362+ ? otherOccurrence . displayTimezone . end . timestamp >
363+ occurrence . displayTimezone . start . timestamp
364+ : otherIndex > index &&
365+ otherOccurrence . displayTimezone . start . timestamp <
366+ occurrence . displayTimezone . end . timestamp ,
367+ )
368+ . map ( ( otherOccurrence ) => firstIndexLookup [ otherOccurrence . key ] ) ,
369+ ) ;
370+ let lastIndex = firstIndex ;
371+ while (
372+ ! usedIndexes . has ( lastIndex + 1 ) &&
373+ lastIndex < maxIndex &&
374+ lastIndex - firstIndex < maxSpan - 1
375+ ) {
376+ lastIndex += 1 ;
377+ }
378+
379+ return { id : occurrence . id , firstIndex, lastIndex } ;
380+ } ) ,
381+ maxIndex,
382+ } ;
383+ }
0 commit comments