empty journey filter

This commit is contained in:
sbriat 2023-09-25 16:03:37 +02:00
parent d8df086c6d
commit eafa3c8bdd
6 changed files with 231 additions and 9 deletions

View File

@ -0,0 +1,10 @@
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { Filter } from './filter.abstract';
/**
* Filter candidates with empty journeys
*/
export class JourneyFilter extends Filter {
filter = async (candidates: CandidateEntity[]): Promise<CandidateEntity[]> =>
candidates.filter((candidate: CandidateEntity) => candidate.hasJourneys());
}

View File

@ -9,6 +9,7 @@ import {
RouteCompleterType, RouteCompleterType,
} from './completer/route.completer'; } from './completer/route.completer';
import { JourneyCompleter } from './completer/journey.completer'; import { JourneyCompleter } from './completer/journey.completer';
import { JourneyFilter } from './filter/journey.filter';
export class PassengerOrientedAlgorithm extends Algorithm { export class PassengerOrientedAlgorithm extends Algorithm {
constructor( constructor(
@ -23,6 +24,7 @@ export class PassengerOrientedAlgorithm extends Algorithm {
new PassengerOrientedGeoFilter(query), new PassengerOrientedGeoFilter(query),
new RouteCompleter(query, RouteCompleterType.DETAILED), new RouteCompleter(query, RouteCompleterType.DETAILED),
new JourneyCompleter(query), new JourneyCompleter(query),
new JourneyFilter(query),
]; ];
} }
} }

View File

@ -44,15 +44,22 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
isDetourValid = (): boolean => isDetourValid = (): boolean =>
this._validateDistanceDetour() && this._validateDurationDetour(); this._validateDistanceDetour() && this._validateDurationDetour();
hasJourneys = (): boolean =>
this.getProps().journeys !== undefined &&
(this.getProps().journeys as Journey[]).length > 0;
/** /**
* Create the journeys based on the driver schedule (the driver 'drives' the carpool !) * 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 ! * This is a tedious process : additional information can be found in deeper methods !
*/ */
createJourneys = (): CandidateEntity => { createJourneys = (): CandidateEntity => {
this.props.journeys = this.props.driverSchedule.map( this.props.journeys = this.props.driverSchedule
(driverScheduleItem: ScheduleItem) => // first we create the journeys
.map((driverScheduleItem: ScheduleItem) =>
this._createJourney(driverScheduleItem), this._createJourney(driverScheduleItem),
); )
// then we filter the ones with invalid pickups
.filter((journey: Journey) => journey.hasValidPickUp());
return this; return this;
}; };

View File

@ -1,5 +1,8 @@
import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library'; import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library';
import { JourneyItem } from './journey-item.value-object'; import { JourneyItem } from './journey-item.value-object';
import { ActorTime } from './actor-time.value-object';
import { Role } from '../ad.types';
import { Target } from '../candidate.types';
/** Note: /** Note:
* Value Objects with multiple properties can contain * Value Objects with multiple properties can contain
@ -25,6 +28,37 @@ export class Journey extends ValueObject<JourneyProps> {
return this.props.journeyItems; return this.props.journeyItems;
} }
hasValidPickUp = (): boolean => {
const passengerDepartureJourneyItem: JourneyItem = this.journeyItems.find(
(journeyItem: JourneyItem) =>
journeyItem.actorTimes.find(
(actorTime: ActorTime) =>
actorTime.role == Role.PASSENGER &&
actorTime.target == Target.START,
) as ActorTime,
) as JourneyItem;
const passengerDepartureActorTime =
passengerDepartureJourneyItem.actorTimes.find(
(actorTime: ActorTime) =>
actorTime.role == Role.PASSENGER && actorTime.target == Target.START,
) as ActorTime;
const driverNeutralActorTime =
passengerDepartureJourneyItem.actorTimes.find(
(actorTime: ActorTime) =>
actorTime.role == Role.DRIVER && actorTime.target == Target.NEUTRAL,
) as ActorTime;
return (
(passengerDepartureActorTime.firstMinDatetime <=
driverNeutralActorTime.firstMaxDatetime &&
driverNeutralActorTime.firstMaxDatetime <=
passengerDepartureActorTime.firstMaxDatetime) ||
(passengerDepartureActorTime.firstMinDatetime <=
driverNeutralActorTime.firstMinDatetime &&
driverNeutralActorTime.firstMinDatetime <=
passengerDepartureActorTime.firstMaxDatetime)
);
};
protected validate(props: JourneyProps): void { protected validate(props: JourneyProps): void {
if (props.firstDate.getUTCDay() != props.lastDate.getUTCDay()) if (props.firstDate.getUTCDay() != props.lastDate.getUTCDay())
throw new ArgumentInvalidException( throw new ArgumentInvalidException(

View File

@ -103,7 +103,7 @@ const schedule4: ScheduleItemProps[] = [
const schedule5: ScheduleItemProps[] = [ const schedule5: ScheduleItemProps[] = [
{ {
day: 0, day: 0,
time: '00:10', time: '00:02',
margin: 900, margin: 900,
}, },
{ {
@ -121,7 +121,15 @@ const schedule6: ScheduleItemProps[] = [
}, },
{ {
day: 6, day: 6,
time: '23:45', time: '23:57',
margin: 900,
},
];
const schedule7: ScheduleItemProps[] = [
{
day: 4,
time: '19:00',
margin: 900, margin: 900,
}, },
]; ];
@ -379,7 +387,7 @@ describe('Candidate entity', () => {
.setCarpoolPath(carpoolPath2) .setCarpoolPath(carpoolPath2)
.setSteps(steps) .setSteps(steps)
.createJourneys(); .createJourneys();
expect(candidateEntity.getProps().journeys).toHaveLength(5); expect(candidateEntity.getProps().journeys).toHaveLength(4);
expect( expect(
( (
candidateEntity.getProps().journeys as Journey[] candidateEntity.getProps().journeys as Journey[]
@ -415,7 +423,7 @@ describe('Candidate entity', () => {
.setCarpoolPath(carpoolPath2) .setCarpoolPath(carpoolPath2)
.setSteps(steps) .setSteps(steps)
.createJourneys(); .createJourneys();
expect(candidateEntity.getProps().journeys).toHaveLength(2); expect(candidateEntity.getProps().journeys).toHaveLength(1);
expect( expect(
(candidateEntity.getProps().journeys as Journey[])[0].journeyItems[1] (candidateEntity.getProps().journeys as Journey[])[0].journeyItems[1]
.actorTimes[0].target, .actorTimes[0].target,
@ -429,7 +437,7 @@ describe('Candidate entity', () => {
( (
candidateEntity.getProps().journeys as Journey[] candidateEntity.getProps().journeys as Journey[]
)[0].journeyItems[1].actorTimes[0].firstDatetime.getUTCMinutes(), )[0].journeyItems[1].actorTimes[0].firstDatetime.getUTCMinutes(),
).toBe(30); ).toBe(22);
expect( expect(
( (
candidateEntity.getProps().journeys as Journey[] candidateEntity.getProps().journeys as Journey[]
@ -439,7 +447,51 @@ describe('Candidate entity', () => {
( (
candidateEntity.getProps().journeys as Journey[] candidateEntity.getProps().journeys as Journey[]
)[0].journeyItems[1].actorTimes[1].firstMinDatetime.getUTCMinutes(), )[0].journeyItems[1].actorTimes[1].firstMinDatetime.getUTCMinutes(),
).toBe(30); ).toBe(42);
});
it('should not create journeys if dates does not match', () => {
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: schedule1,
passengerSchedule: schedule7,
spacetimeDetourRatio,
})
.setCarpoolPath(carpoolPath2)
.setSteps(steps)
.createJourneys();
expect(candidateEntity.getProps().journeys).toHaveLength(0);
expect(candidateEntity.hasJourneys()).toBeFalsy();
});
it('should not verify journeys if journeys is undefined', () => {
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: schedule1,
passengerSchedule: schedule7,
spacetimeDetourRatio,
})
.setCarpoolPath(carpoolPath2)
.setSteps(steps);
expect(candidateEntity.hasJourneys()).toBeFalsy();
}); });
}); });
}); });

View File

@ -0,0 +1,117 @@
import { JourneyFilter } from '@modules/ad/core/application/queries/match/filter/journey.filter';
import { MatchQuery } 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, Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
const originWaypoint: Waypoint = {
position: 0,
lat: 48.689445,
lon: 6.17651,
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
};
const destinationWaypoint: Waypoint = {
position: 1,
lat: 48.8566,
lon: 2.3522,
locality: 'Paris',
postalCode: '75000',
country: 'France',
};
const matchQuery = new MatchQuery(
{
algorithmType: AlgorithmType.PASSENGER_ORIENTED,
driver: true,
passenger: true,
frequency: Frequency.PUNCTUAL,
fromDate: '2023-08-28',
toDate: '2023-08-28',
schedule: [
{
time: '07:05',
},
],
strict: false,
waypoints: [originWaypoint, destinationWaypoint],
},
{
getBasic: jest.fn(),
getDetailed: jest.fn(),
},
);
const candidate: CandidateEntity = CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER,
dateInterval: {
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,
},
],
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,
},
});
describe('Passenger oriented time filter', () => {
it('should not filter valid candidates', async () => {
const passengerOrientedTimeFilter: JourneyFilter = new JourneyFilter(
matchQuery,
);
candidate.hasJourneys = () => true;
const filteredCandidates: CandidateEntity[] =
await passengerOrientedTimeFilter.filter([candidate]);
expect(filteredCandidates.length).toBe(1);
});
it('should filter invalid candidates', async () => {
const passengerOrientedTimeFilter: JourneyFilter = new JourneyFilter(
matchQuery,
);
candidate.hasJourneys = () => false;
const filteredCandidates: CandidateEntity[] =
await passengerOrientedTimeFilter.filter([candidate]);
expect(filteredCandidates.length).toBe(0);
});
});