handle empty schedule in candidates
This commit is contained in:
parent
90ae3cf9cb
commit
5f8dd8b4a0
|
@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
Loading…
Reference in New Issue