compute journeys with tests

This commit is contained in:
sbriat 2023-09-25 11:42:22 +02:00
parent 467d8a84f8
commit d8df086c6d
5 changed files with 422 additions and 247 deletions

View File

@ -20,7 +20,7 @@ export abstract class Algorithm {
for (const processor of this.processors) {
this.candidates = await processor.execute(this.candidates);
}
console.log(JSON.stringify(this.candidates, null, 2));
// console.log(JSON.stringify(this.candidates, null, 2));
return this.candidates.map((candidate: CandidateEntity) =>
MatchEntity.create({ adId: candidate.id }),
);

View File

@ -63,14 +63,16 @@ export class CalendarTools {
};
/**
* Returns a date from a date and time as strings, adding optional seconds
* Returns a date from a date (as a date) and a time (as a string), adding optional seconds
*/
static datetimeFromString = (
date: string,
static datetimeWithSeconds = (
date: Date,
time: string,
additionalSeconds = 0,
): Date => {
const datetime = new Date(`${date}T${time}:00Z`);
const datetime: Date = new Date(date);
datetime.setUTCHours(parseInt(time.split(':')[0]));
datetime.setUTCMinutes(parseInt(time.split(':')[1]));
datetime.setUTCSeconds(additionalSeconds);
return datetime;
};
@ -79,7 +81,7 @@ export class CalendarTools {
* Returns dates from a day and time based on unix epoch day
* (1970-01-01 is day 4)
* The method returns an array of dates because for edges (day 0 and 6)
* we need to return 2 possibilities
* we need to return 2 possibilities : one for the previous week, one for the next week
*/
static epochDaysFromTime = (weekDay: number, time: string): Date[] => {
if (weekDay < 0 || weekDay > 6)

View File

@ -46,6 +46,7 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
/**
* Create the journeys based on the driver schedule (the driver 'drives' the carpool !)
* This is a tedious process : additional information can be found in deeper methods !
*/
createJourneys = (): CandidateEntity => {
this.props.journeys = this.props.driverSchedule.map(
@ -90,6 +91,13 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
this._createJourneyItem(carpoolPathItem, index, driverScheduleItem),
) as JourneyItem[];
/**
* Create a journey item based on a carpool path item and driver schedule item
* The stepIndex is used to get the duration to reach the carpool path item
* from the steps prop (computed previously by a georouter)
* There MUST be a one/one relation between the carpool path items indexes
* and the steps indexes.
*/
private _createJourneyItem = (
carpoolPathItem: CarpoolPathItem,
stepIndex: number,
@ -123,42 +131,55 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
actor.target == Target.START
? 0
: duration;
const firstDate: Date = CalendarTools.firstDate(
scheduleItem.day,
this.props.dateInterval,
);
const lastDate: Date = CalendarTools.lastDate(
scheduleItem.day,
this.props.dateInterval,
);
return new ActorTime({
role: actor.role,
target: actor.target,
firstDatetime: CalendarTools.datetimeFromString(
this.props.dateInterval.lowerDate,
firstDatetime: CalendarTools.datetimeWithSeconds(
firstDate,
scheduleItem.time,
effectiveDuration,
),
firstMinDatetime: CalendarTools.datetimeFromString(
this.props.dateInterval.lowerDate,
firstMinDatetime: CalendarTools.datetimeWithSeconds(
firstDate,
scheduleItem.time,
-scheduleItem.margin + effectiveDuration,
),
firstMaxDatetime: CalendarTools.datetimeFromString(
this.props.dateInterval.lowerDate,
firstMaxDatetime: CalendarTools.datetimeWithSeconds(
firstDate,
scheduleItem.time,
scheduleItem.margin + effectiveDuration,
),
lastDatetime: CalendarTools.datetimeFromString(
this.props.dateInterval.higherDate,
lastDatetime: CalendarTools.datetimeWithSeconds(
lastDate,
scheduleItem.time,
effectiveDuration,
),
lastMinDatetime: CalendarTools.datetimeFromString(
this.props.dateInterval.higherDate,
lastMinDatetime: CalendarTools.datetimeWithSeconds(
lastDate,
scheduleItem.time,
-scheduleItem.margin + effectiveDuration,
),
lastMaxDatetime: CalendarTools.datetimeFromString(
this.props.dateInterval.higherDate,
lastMaxDatetime: CalendarTools.datetimeWithSeconds(
lastDate,
scheduleItem.time,
scheduleItem.margin + effectiveDuration,
),
});
};
/**
* Get the closest (in time) passenger schedule item for a given driver schedule item
* This is mandatory as we can't rely only on the day of the schedule item :
* items on different days can match when playing with margins around midnight
*/
private _closestPassengerScheduleItem = (
driverScheduleItem: ScheduleItem,
): ScheduleItem =>
@ -179,8 +200,12 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
: currentScheduleItemGap,
).scheduleItem;
/**
* 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
// first map the passenger schedule to "real" dates (we use unix epoch date as base)
.map(
(scheduleItem: ScheduleItem) =>
<ScheduleItemRange>{
@ -191,16 +216,21 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
),
},
)
// then compute the duration in seconds to the given date
// for each "real" date computed in step 1
.map((scheduleItemRange: ScheduleItemRange) => ({
scheduleItem: scheduleItemRange.scheduleItem,
gap: scheduleItemRange.range
// compute the duration
.map((scheduleDate: Date) =>
Math.round(Math.abs(scheduleDate.getTime() - date.getTime())),
)
// keep the lowest duration
.reduce((previousGap: number, currentGap: number) =>
previousGap < currentGap ? previousGap : currentGap,
),
}))
// finally, keep the passenger schedule item with the lowest duration
.reduce(
(
previousScheduleItemGap: ScheduleItemGap,

View File

@ -90,27 +90,26 @@ describe('Calendar tools service', () => {
describe('Datetime from string', () => {
it('should return a date with time from a string without additional seconds', () => {
const datetime: Date = CalendarTools.datetimeFromString(
'2023-09-01',
const datetime: Date = CalendarTools.datetimeWithSeconds(
new Date('2023-09-01'),
'07:12',
);
expect(datetime.getUTCMinutes()).toBe(12);
});
it('should return a date with time from a string with additional seconds', () => {
const datetime: Date = CalendarTools.datetimeFromString(
'2023-09-01',
const datetime: Date = CalendarTools.datetimeWithSeconds(
new Date('2023-09-01'),
'07:12',
60,
);
expect(datetime.getUTCMinutes()).toBe(13);
});
it('should return a date with time from a string with negative additional seconds', () => {
const datetime: Date = CalendarTools.datetimeFromString(
'2023-09-01',
const datetime: Date = CalendarTools.datetimeWithSeconds(
new Date('2023-09-01'),
'07:00',
-60,
);
console.log(datetime);
expect(datetime.getUTCHours()).toBe(6);
expect(datetime.getUTCMinutes()).toBe(59);
});

View File

@ -1,7 +1,244 @@
import { Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { Target } from '@modules/ad/core/domain/candidate.types';
import {
SpacetimeDetourRatio,
Target,
} 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 { 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';
const waypointsSet1: PointProps[] = [
{
lat: 48.678454,
lon: 6.189745,
},
{
lat: 48.84877,
lon: 2.398457,
},
];
const waypointsSet2: PointProps[] = [
{
lat: 48.689445,
lon: 6.17651,
},
{
lat: 48.8566,
lon: 2.3522,
},
];
const schedule1: ScheduleItemProps[] = [
{
day: 1,
time: '07:00',
margin: 900,
},
];
const schedule2: ScheduleItemProps[] = [
{
day: 1,
time: '07:10',
margin: 900,
},
];
const schedule3: ScheduleItemProps[] = [
{
day: 1,
time: '06:30',
margin: 900,
},
{
day: 2,
time: '06:30',
margin: 900,
},
{
day: 3,
time: '06:00',
margin: 900,
},
{
day: 4,
time: '06:30',
margin: 900,
},
{
day: 5,
time: '06:30',
margin: 900,
},
];
const schedule4: ScheduleItemProps[] = [
{
day: 1,
time: '06:50',
margin: 900,
},
{
day: 2,
time: '06:50',
margin: 900,
},
{
day: 4,
time: '06:50',
margin: 900,
},
{
day: 5,
time: '06:50',
margin: 900,
},
];
const schedule5: ScheduleItemProps[] = [
{
day: 0,
time: '00:10',
margin: 900,
},
{
day: 1,
time: '07:05',
margin: 900,
},
];
const schedule6: ScheduleItemProps[] = [
{
day: 1,
time: '23:10',
margin: 900,
},
{
day: 6,
time: '23:45',
margin: 900,
},
];
const spacetimeDetourRatio: SpacetimeDetourRatio = {
maxDistanceDetourRatio: 0.3,
maxDurationDetourRatio: 0.3,
};
const carpoolPath1: CarpoolPathItemProps[] = [
{
lat: 48.689445,
lon: 6.17651,
actors: [
new Actor({
role: Role.DRIVER,
target: Target.START,
}),
new Actor({
role: Role.PASSENGER,
target: Target.START,
}),
],
},
{
lat: 48.8566,
lon: 2.3522,
actors: [
new Actor({
role: Role.DRIVER,
target: Target.FINISH,
}),
new Actor({
role: Role.PASSENGER,
target: Target.FINISH,
}),
],
},
];
const carpoolPath2: CarpoolPathItemProps[] = [
{
lat: 48.689445,
lon: 6.17651,
actors: [
new Actor({
role: Role.DRIVER,
target: Target.START,
}),
],
},
{
lat: 48.678451,
lon: 6.168784,
actors: [
new Actor({
role: Role.DRIVER,
target: Target.NEUTRAL,
}),
new Actor({
role: Role.PASSENGER,
target: Target.START,
}),
],
},
{
lat: 48.848715,
lon: 2.36985,
actors: [
new Actor({
role: Role.DRIVER,
target: Target.NEUTRAL,
}),
new Actor({
role: Role.PASSENGER,
target: Target.FINISH,
}),
],
},
{
lat: 48.8566,
lon: 2.3522,
actors: [
new Actor({
role: Role.DRIVER,
target: Target.FINISH,
}),
],
},
];
const steps: StepProps[] = [
{
lat: 48.689445,
lon: 6.17651,
duration: 0,
distance: 0,
},
{
lat: 48.678451,
lon: 6.168784,
duration: 1254,
distance: 33462,
},
{
lat: 48.848715,
lon: 2.36985,
duration: 12477,
distance: 343654,
},
{
lat: 48.8566,
lon: 2.3522,
duration: 13548,
distance: 350145,
},
];
describe('Candidate entity', () => {
it('should create a new candidate entity', () => {
@ -12,46 +249,13 @@ describe('Candidate entity', () => {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: [
{
lat: 48.678454,
lon: 6.189745,
},
{
lat: 48.84877,
lon: 2.398457,
},
],
passengerWaypoints: [
{
lat: 48.689445,
lon: 6.17651,
},
{
lat: 48.8566,
lon: 2.3522,
},
],
driverWaypoints: waypointsSet1,
passengerWaypoints: waypointsSet2,
driverDistance: 350145,
driverDuration: 13548,
driverSchedule: [
{
day: 0,
time: '07:00',
margin: 900,
},
],
passengerSchedule: [
{
day: 0,
time: '07:10',
margin: 900,
},
],
spacetimeDetourRatio: {
maxDistanceDetourRatio: 0.3,
maxDurationDetourRatio: 0.3,
},
driverSchedule: schedule1,
passengerSchedule: schedule2,
spacetimeDetourRatio,
});
expect(candidateEntity.id.length).toBe(36);
});
@ -64,76 +268,14 @@ describe('Candidate entity', () => {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: [
{
lat: 48.689445,
lon: 6.17651,
},
{
lat: 48.8566,
lon: 2.3522,
},
],
passengerWaypoints: [
{
lat: 48.689445,
lon: 6.17651,
},
{
lat: 48.8566,
lon: 2.3522,
},
],
driverWaypoints: waypointsSet2,
passengerWaypoints: waypointsSet2,
driverDistance: 350145,
driverDuration: 13548,
driverSchedule: [
{
day: 0,
time: '07:00',
margin: 900,
},
],
passengerSchedule: [
{
day: 0,
time: '07:10',
margin: 900,
},
],
spacetimeDetourRatio: {
maxDistanceDetourRatio: 0.3,
maxDurationDetourRatio: 0.3,
},
}).setCarpoolPath([
{
lat: 48.689445,
lon: 6.17651,
actors: [
new Actor({
role: Role.DRIVER,
target: Target.START,
}),
new Actor({
role: Role.PASSENGER,
target: Target.START,
}),
],
},
{
lat: 48.8566,
lon: 2.3522,
actors: [
new Actor({
role: Role.DRIVER,
target: Target.FINISH,
}),
new Actor({
role: Role.PASSENGER,
target: Target.FINISH,
}),
],
},
]);
driverSchedule: schedule1,
passengerSchedule: schedule2,
spacetimeDetourRatio,
}).setCarpoolPath(carpoolPath1);
expect(candidateEntity.getProps().carpoolPath).toHaveLength(2);
});
@ -145,46 +287,13 @@ describe('Candidate entity', () => {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: [
{
lat: 48.678454,
lon: 6.189745,
},
{
lat: 48.84877,
lon: 2.398457,
},
],
passengerWaypoints: [
{
lat: 48.689445,
lon: 6.17651,
},
{
lat: 48.8566,
lon: 2.3522,
},
],
driverWaypoints: waypointsSet1,
passengerWaypoints: waypointsSet2,
driverDistance: 350145,
driverDuration: 13548,
driverSchedule: [
{
day: 0,
time: '07:00',
margin: 900,
},
],
passengerSchedule: [
{
day: 0,
time: '07:10',
margin: 900,
},
],
spacetimeDetourRatio: {
maxDistanceDetourRatio: 0.3,
maxDurationDetourRatio: 0.3,
},
driverSchedule: schedule1,
passengerSchedule: schedule2,
spacetimeDetourRatio,
}).setMetrics(352688, 14587);
expect(candidateEntity.getProps().distance).toBe(352688);
expect(candidateEntity.getProps().duration).toBe(14587);
@ -199,46 +308,13 @@ describe('Candidate entity', () => {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: [
{
lat: 48.678454,
lon: 6.189745,
},
{
lat: 48.84877,
lon: 2.398457,
},
],
passengerWaypoints: [
{
lat: 48.849445,
lon: 6.68651,
},
{
lat: 47.18746,
lon: 2.89742,
},
],
driverWaypoints: waypointsSet1,
passengerWaypoints: waypointsSet2,
driverDistance: 350145,
driverDuration: 13548,
driverSchedule: [
{
day: 0,
time: '07:00',
margin: 900,
},
],
passengerSchedule: [
{
day: 0,
time: '07:10',
margin: 900,
},
],
spacetimeDetourRatio: {
maxDistanceDetourRatio: 0.3,
maxDurationDetourRatio: 0.3,
},
driverSchedule: schedule1,
passengerSchedule: schedule2,
spacetimeDetourRatio,
}).setMetrics(458690, 13980);
expect(candidateEntity.isDetourValid()).toBeFalsy();
});
@ -250,52 +326,120 @@ describe('Candidate entity', () => {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: [
{
lat: 48.678454,
lon: 6.189745,
},
{
lat: 48.84877,
lon: 2.398457,
},
],
passengerWaypoints: [
{
lat: 48.849445,
lon: 6.68651,
},
{
lat: 47.18746,
lon: 2.89742,
},
],
driverWaypoints: waypointsSet1,
passengerWaypoints: waypointsSet2,
driverDistance: 350145,
driverDuration: 13548,
driverSchedule: [
{
day: 0,
time: '07:00',
margin: 900,
},
],
passengerSchedule: [
{
day: 0,
time: '07:10',
margin: 900,
},
],
spacetimeDetourRatio: {
maxDistanceDetourRatio: 0.3,
maxDurationDetourRatio: 0.3,
},
driverSchedule: schedule1,
passengerSchedule: schedule2,
spacetimeDetourRatio,
}).setMetrics(352368, 18314);
expect(candidateEntity.isDetourValid()).toBeFalsy();
});
});
describe('Journeys', () => {
it('should create journeys', () => {});
it('should create journeys for a single date', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
role: Role.PASSENGER,
dateInterval: {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: waypointsSet1,
passengerWaypoints: waypointsSet2,
driverDistance: 350145,
driverDuration: 13548,
driverSchedule: schedule1,
passengerSchedule: schedule2,
spacetimeDetourRatio,
})
.setCarpoolPath(carpoolPath2)
.setSteps(steps)
.createJourneys();
expect(candidateEntity.getProps().journeys).toHaveLength(1);
});
it('should create journeys for multiple dates', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
role: Role.PASSENGER,
dateInterval: {
lowerDate: '2023-09-01',
higherDate: '2024-09-01',
},
driverWaypoints: waypointsSet1,
passengerWaypoints: waypointsSet2,
driverDistance: 350145,
driverDuration: 13548,
driverSchedule: schedule3,
passengerSchedule: schedule4,
spacetimeDetourRatio,
})
.setCarpoolPath(carpoolPath2)
.setSteps(steps)
.createJourneys();
expect(candidateEntity.getProps().journeys).toHaveLength(5);
expect(
(
candidateEntity.getProps().journeys as Journey[]
)[0].firstDate.getDate(),
).toBe(4);
expect(
(
candidateEntity.getProps().journeys as Journey[]
)[0].journeyItems[1].actorTimes[1].firstMinDatetime.getDate(),
).toBe(4);
expect(
(
candidateEntity.getProps().journeys as Journey[]
)[1].journeyItems[1].actorTimes[1].firstMinDatetime.getDate(),
).toBe(5);
});
it('should create journeys for multiple dates, including week edges (saturday/sunday)', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
role: Role.PASSENGER,
dateInterval: {
lowerDate: '2023-09-01',
higherDate: '2024-09-01',
},
driverWaypoints: waypointsSet1,
passengerWaypoints: waypointsSet2,
driverDistance: 350145,
driverDuration: 13548,
driverSchedule: schedule5,
passengerSchedule: schedule6,
spacetimeDetourRatio,
})
.setCarpoolPath(carpoolPath2)
.setSteps(steps)
.createJourneys();
expect(candidateEntity.getProps().journeys).toHaveLength(2);
expect(
(candidateEntity.getProps().journeys as Journey[])[0].journeyItems[1]
.actorTimes[0].target,
).toBe(Target.NEUTRAL);
expect(
(
candidateEntity.getProps().journeys as Journey[]
)[0].journeyItems[1].actorTimes[0].firstDatetime.getUTCHours(),
).toBe(0);
expect(
(
candidateEntity.getProps().journeys as Journey[]
)[0].journeyItems[1].actorTimes[0].firstDatetime.getUTCMinutes(),
).toBe(30);
expect(
(
candidateEntity.getProps().journeys as Journey[]
)[0].journeyItems[1].actorTimes[1].firstMinDatetime.getUTCHours(),
).toBe(23);
expect(
(
candidateEntity.getProps().journeys as Journey[]
)[0].journeyItems[1].actorTimes[1].firstMinDatetime.getUTCMinutes(),
).toBe(30);
});
});
});