handle empty schedule in candidates

This commit is contained in:
Sylvain Briat 2024-03-28 17:19:54 +01:00 committed by sbriat
parent 90ae3cf9cb
commit 5f8dd8b4a0
7 changed files with 259 additions and 16 deletions

View File

@ -1,4 +1,8 @@
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import {
AggregateRoot,
AggregateID,
ArgumentInvalidException,
} from '@mobicoop/ddd-library';
import {
CandidateProps,
CreateCandidateProps,
@ -9,7 +13,10 @@ import {
CarpoolPathItemProps,
} from './value-objects/carpool-path-item.value-object';
import { Step, StepProps } from './value-objects/step.value-object';
import { ScheduleItem } from './value-objects/schedule-item.value-object';
import {
ScheduleItem,
ScheduleItemProps,
} from './value-objects/schedule-item.value-object';
import { Journey } from './value-objects/journey.value-object';
import { CalendarTools } from './calendar-tools.service';
import { JourneyItem } from './value-objects/journey-item.value-object';
@ -53,8 +60,11 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
* This is a tedious process : additional information can be found in deeper methods !
*/
createJourneys = (): CandidateEntity => {
// driver and passenger schedules are mandatory
if (!this.props.driverSchedule) this._createDriverSchedule();
if (!this.props.passengerSchedule) this._createPassengerSchedule();
try {
this.props.journeys = this.props.driverSchedule
this.props.journeys = (this.props.driverSchedule as ScheduleItemProps[])
// first we create the journeys
.map((driverScheduleItem: ScheduleItem) =>
this._createJourney(driverScheduleItem),
@ -82,6 +92,122 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
(1 + this.props.spacetimeDetourRatio.maxDistanceDetourRatio)
: false;
/**
* Create the driver schedule based on the passenger schedule
*/
private _createDriverSchedule = (): void => {
if (this.props.passengerSchedule) {
let driverSchedule: ScheduleItemProps[] =
this.props.passengerSchedule.map(
(scheduleItemProps: ScheduleItemProps) => ({
day: scheduleItemProps.day,
time: scheduleItemProps.time,
margin: scheduleItemProps.margin,
}),
);
// adjust the driver theoretical schedule :
// we guess the ideal driver departure time based on the duration to
// reach the passenger starting point from the driver starting point
driverSchedule = driverSchedule.map(
(scheduleItemProps: ScheduleItemProps) => {
const driverDate: Date = CalendarTools.firstDate(
scheduleItemProps.day,
this.props.dateInterval,
);
const driverStartDatetime: Date = CalendarTools.datetimeWithSeconds(
driverDate,
scheduleItemProps.time,
-this._passengerStartDuration(),
);
return {
day: driverDate.getUTCDay(),
margin: scheduleItemProps.margin,
time: `${driverStartDatetime
.getUTCHours()
.toString()
.padStart(2, '0')}:${driverStartDatetime
.getUTCMinutes()
.toString()
.padStart(2, '0')}`,
};
},
);
this.props.driverSchedule = driverSchedule.map(
(scheduleItemProps: ScheduleItemProps) => ({
day: scheduleItemProps.day,
time: scheduleItemProps.time,
margin: scheduleItemProps.margin,
}),
);
}
};
/**
* Return the duration to reach the passenger starting point from the driver starting point
*/
private _passengerStartDuration = (): number => {
let passengerStartStepIndex = 0;
this.props.carpoolPath?.forEach(
(carpoolPathItem: CarpoolPathItem, index: number) => {
carpoolPathItem.actors.forEach((actor: Actor) => {
if (actor.role == Role.PASSENGER && actor.target == Target.START)
passengerStartStepIndex = index;
});
},
);
return (this.props.steps as Step[])[passengerStartStepIndex].duration;
};
/**
* Create the passenger schedule based on the driver schedule
*/
private _createPassengerSchedule = (): void => {
if (this.props.driverSchedule) {
let passengerSchedule: ScheduleItemProps[] =
this.props.driverSchedule.map(
(scheduleItemProps: ScheduleItemProps) => ({
day: scheduleItemProps.day,
time: scheduleItemProps.time,
margin: scheduleItemProps.margin,
}),
);
// adjust the passenger theoretical schedule :
// we guess the ideal passenger departure time based on the duration to
// reach the passenger starting point from the driver starting point
passengerSchedule = passengerSchedule.map(
(scheduleItemProps: ScheduleItemProps) => {
const passengerDate: Date = CalendarTools.firstDate(
scheduleItemProps.day,
this.props.dateInterval,
);
const passengeStartDatetime: Date = CalendarTools.datetimeWithSeconds(
passengerDate,
scheduleItemProps.time,
this._passengerStartDuration(),
);
return {
day: passengerDate.getUTCDay(),
margin: scheduleItemProps.margin,
time: `${passengeStartDatetime
.getUTCHours()
.toString()
.padStart(2, '0')}:${passengeStartDatetime
.getUTCMinutes()
.toString()
.padStart(2, '0')}`,
};
},
);
this.props.passengerSchedule = passengerSchedule.map(
(scheduleItemProps: ScheduleItemProps) => ({
day: scheduleItemProps.day,
time: scheduleItemProps.time,
margin: scheduleItemProps.margin,
}),
);
}
};
private _createJourney = (driverScheduleItem: ScheduleItem): Journey =>
new Journey({
firstDate: CalendarTools.firstDate(
@ -216,7 +342,7 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
* Find the passenger schedule item with the minimum duration between a given date and the dates of the passenger schedule
*/
private _minPassengerScheduleItemGapForDate = (date: Date): ScheduleItemGap =>
this.props.passengerSchedule
(this.props.passengerSchedule as ScheduleItemProps[])
// first map the passenger schedule to "real" dates (we use unix epoch date as base)
.map(
(scheduleItem: ScheduleItem) =>
@ -255,6 +381,10 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
validate(): void {
// entity business rules validation to protect it's invariant before saving entity to a database
if (!this.props.driverSchedule && !this.props.passengerSchedule)
throw new ArgumentInvalidException(
'at least the driver or the passenger schedule is required',
);
}
}

View File

@ -11,8 +11,8 @@ export interface CandidateProps {
frequency: Frequency;
driverWaypoints: PointProps[];
passengerWaypoints: PointProps[];
driverSchedule: ScheduleItemProps[];
passengerSchedule: ScheduleItemProps[];
driverSchedule?: ScheduleItemProps[];
passengerSchedule?: ScheduleItemProps[];
driverDistance: number;
driverDuration: number;
dateInterval: DateInterval;
@ -33,8 +33,8 @@ export interface CreateCandidateProps {
driverDuration: number;
driverWaypoints: PointProps[];
passengerWaypoints: PointProps[];
driverSchedule: ScheduleItemProps[];
passengerSchedule: ScheduleItemProps[];
driverSchedule?: ScheduleItemProps[];
passengerSchedule?: ScheduleItemProps[];
spacetimeDetourRatio: SpacetimeDetourRatio;
dateInterval: DateInterval;
}

View File

@ -46,6 +46,23 @@ export class Journey extends ValueObject<JourneyProps> {
const driverActorTime = passengerDepartureJourneyItem.actorTimes.find(
(actorTime: ActorTime) => actorTime.role == Role.DRIVER,
) as ActorTime;
// return (
// // 1
// (driverActorTime.firstMinDatetime >=
// passengerDepartureActorTime.firstMinDatetime &&
// driverActorTime.firstMaxDatetime <=
// passengerDepartureActorTime.firstMaxDatetime) ||
// // 2 & 4
// (driverActorTime.firstMinDatetime <=
// passengerDepartureActorTime.firstMinDatetime &&
// driverActorTime.firstMaxDatetime >=
// passengerDepartureActorTime.firstMinDatetime) ||
// // 3
// (driverActorTime.firstMinDatetime >=
// passengerDepartureActorTime.firstMinDatetime &&
// driverActorTime.firstMinDatetime <=
// passengerDepartureActorTime.firstMaxDatetime)
// );
return (
(passengerDepartureActorTime.firstMinDatetime <=
driverActorTime.firstMaxDatetime &&

View File

@ -14,7 +14,7 @@ export interface MatchQueryProps {
frequency: Frequency;
fromDate: string;
toDate: string;
schedule: ScheduleItemProps[];
schedule?: ScheduleItemProps[];
seatsProposed: number;
seatsRequested: number;
strict: boolean;
@ -50,7 +50,7 @@ export class MatchQuery extends ValueObject<MatchQueryProps> {
return this.props.toDate;
}
get schedule(): ScheduleItemProps[] {
get schedule(): ScheduleItemProps[] | undefined {
return this.props.schedule;
}

View File

@ -1,3 +1,4 @@
import { ArgumentInvalidException } from '@mobicoop/ddd-library';
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import {
@ -6,7 +7,10 @@ import {
} from '@modules/ad/core/domain/candidate.types';
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
import { CarpoolPathItemProps } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object';
import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object';
import {
Journey,
JourneyProps,
} from '@modules/ad/core/domain/value-objects/journey.value-object';
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
import { StepProps } from '@modules/ad/core/domain/value-objects/step.value-object';
@ -374,6 +378,95 @@ describe('Candidate entity', () => {
.createJourneys();
expect(candidateEntity.getProps().journeys).toHaveLength(1);
});
it('should create journeys for a single date without driver schedule', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
role: Role.PASSENGER,
frequency: Frequency.PUNCTUAL,
dateInterval: {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: waypointsSet1,
passengerWaypoints: waypointsSet2,
driverDistance: 350145,
driverDuration: 13548,
driverSchedule: undefined,
passengerSchedule: schedule2,
spacetimeDetourRatio,
})
.setCarpoolPath(carpoolPath2)
.setSteps(steps)
.createJourneys();
expect(candidateEntity.getProps().journeys).toHaveLength(1);
// computed driver start time should be 06:49
expect(
(
candidateEntity.getProps().journeys as JourneyProps[]
)[0].journeyItems[0].actorTimes[0].firstDatetime.getUTCMinutes(),
).toBe(49);
expect(
(
candidateEntity.getProps().journeys as JourneyProps[]
)[0].journeyItems[0].actorTimes[0].firstDatetime.getUTCHours(),
).toBe(6);
});
it('should create journeys for a single date without passenger schedule', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
role: Role.PASSENGER,
frequency: Frequency.PUNCTUAL,
dateInterval: {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: waypointsSet1,
passengerWaypoints: waypointsSet2,
driverDistance: 350145,
driverDuration: 13548,
driverSchedule: schedule1,
passengerSchedule: undefined,
spacetimeDetourRatio,
})
.setCarpoolPath(carpoolPath2)
.setSteps(steps)
.createJourneys();
expect(candidateEntity.getProps().journeys).toHaveLength(1);
// computed passenger start time should be 07:20
expect(
(
candidateEntity.getProps().journeys as JourneyProps[]
)[0].journeyItems[1].actorTimes[0].firstDatetime.getUTCMinutes(),
).toBe(20);
expect(
(
candidateEntity.getProps().journeys as JourneyProps[]
)[0].journeyItems[1].actorTimes[0].firstDatetime.getUTCHours(),
).toBe(7);
});
it('should throw without driver and passenger schedule', () => {
expect(() =>
CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
role: Role.PASSENGER,
frequency: Frequency.PUNCTUAL,
dateInterval: {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: waypointsSet1,
passengerWaypoints: waypointsSet2,
driverDistance: 350145,
driverDuration: 13548,
driverSchedule: undefined,
passengerSchedule: undefined,
spacetimeDetourRatio,
})
.setCarpoolPath(carpoolPath2)
.setSteps(steps)
.createJourneys(),
).toThrow(ArgumentInvalidException);
});
it('should create journeys for multiple dates', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',

View File

@ -44,7 +44,7 @@ describe('Match Query value object', () => {
expect(matchQueryVO.frequency).toBe(Frequency.PUNCTUAL);
expect(matchQueryVO.fromDate).toBe('2023-09-01');
expect(matchQueryVO.toDate).toBe('2023-09-01');
expect(matchQueryVO.schedule.length).toBe(1);
expect(matchQueryVO.schedule?.length).toBe(1);
expect(matchQueryVO.seatsProposed).toBe(3);
expect(matchQueryVO.seatsRequested).toBe(1);
expect(matchQueryVO.strict).toBe(false);

View File

@ -1,6 +1,9 @@
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port';
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
import {
MatchQuery,
ScheduleItem,
} from '@modules/ad/core/application/queries/match/match.query';
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
import { Frequency } from '@modules/ad/core/domain/ad.types';
@ -135,9 +138,9 @@ describe('Match Query', () => {
expect(matchQuery.maxDetourDurationRatio).toBe(0.3);
expect(matchQuery.fromDate).toBe('2023-08-27');
expect(matchQuery.toDate).toBe('2023-08-27');
expect(matchQuery.schedule[0].day).toBe(0);
expect(matchQuery.schedule[0].time).toBe('23:05');
expect(matchQuery.schedule[0].margin).toBe(900);
expect((matchQuery.schedule as ScheduleItem[])[0].day).toBe(0);
expect((matchQuery.schedule as ScheduleItem[])[0].time).toBe('23:05');
expect((matchQuery.schedule as ScheduleItem[])[0].margin).toBe(900);
});
it('should set good values for seats', async () => {