wip - create journeys - no tests yet

This commit is contained in:
sbriat 2023-09-22 16:37:52 +02:00
parent dfc8dbcc51
commit 467d8a84f8
27 changed files with 860 additions and 622 deletions

View File

@ -20,7 +20,7 @@ export abstract class Algorithm {
for (const processor of this.processors) { for (const processor of this.processors) {
this.candidates = await processor.execute(this.candidates); 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) => return this.candidates.map((candidate: CandidateEntity) =>
MatchEntity.create({ adId: candidate.id }), MatchEntity.create({ adId: candidate.id }),
); );

View File

@ -5,7 +5,5 @@ export class JourneyCompleter extends Completer {
complete = async ( complete = async (
candidates: CandidateEntity[], candidates: CandidateEntity[],
): Promise<CandidateEntity[]> => ): Promise<CandidateEntity[]> =>
candidates.map((candidate: CandidateEntity) => candidates.map((candidate: CandidateEntity) => candidate.createJourneys());
candidate.createJourneys(this.query.fromDate, this.query.toDate),
);
} }

View File

@ -1,8 +1,8 @@
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { Completer } from './completer.abstract'; import { Completer } from './completer.abstract';
import { MatchQuery } from '../match.query'; import { MatchQuery } from '../match.query';
import { CarpoolStep } from '@modules/ad/core/domain/value-objects/carpool-step.value-object';
import { Step } from '../../../types/step.type'; import { Step } from '../../../types/step.type';
import { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object';
export class RouteCompleter extends Completer { export class RouteCompleter extends Completer {
protected readonly type: RouteCompleterType; protected readonly type: RouteCompleterType;
@ -19,8 +19,8 @@ export class RouteCompleter extends Completer {
switch (this.type) { switch (this.type) {
case RouteCompleterType.BASIC: case RouteCompleterType.BASIC:
const basicCandidateRoute = await this.query.routeProvider.getBasic( const basicCandidateRoute = await this.query.routeProvider.getBasic(
(candidate.getProps().carpoolSteps as CarpoolStep[]).map( (candidate.getProps().carpoolPath as CarpoolPathItem[]).map(
(carpoolStep: CarpoolStep) => carpoolStep.point, (carpoolPathItem: CarpoolPathItem) => carpoolPathItem,
), ),
); );
candidate.setMetrics( candidate.setMetrics(
@ -31,8 +31,8 @@ export class RouteCompleter extends Completer {
case RouteCompleterType.DETAILED: case RouteCompleterType.DETAILED:
const detailedCandidateRoute = const detailedCandidateRoute =
await this.query.routeProvider.getDetailed( await this.query.routeProvider.getDetailed(
(candidate.getProps().carpoolSteps as CarpoolStep[]).map( (candidate.getProps().carpoolPath as CarpoolPathItem[]).map(
(carpoolStep: CarpoolStep) => carpoolStep.point, (carpoolPathItem: CarpoolPathItem) => carpoolPathItem,
), ),
); );
candidate.setSteps(detailedCandidateRoute.steps as Step[]); candidate.setSteps(detailedCandidateRoute.steps as Step[]);

View File

@ -153,7 +153,7 @@ export class MatchQuery extends QueryBase {
); );
this.schedule = this.schedule.map((scheduleItem: ScheduleItem) => ({ this.schedule = this.schedule.map((scheduleItem: ScheduleItem) => ({
day: datetimeTransformer.day( day: datetimeTransformer.day(
scheduleItem.day ?? new Date(this.fromDate).getDay(), scheduleItem.day ?? new Date(this.fromDate).getUTCDay(),
{ {
date: this.fromDate, date: this.fromDate,
time: scheduleItem.time, time: scheduleItem.time,

View File

@ -36,6 +36,16 @@ export class PassengerOrientedSelector extends Selector {
CandidateEntity.create({ CandidateEntity.create({
id: adEntity.id, id: adEntity.id,
role: adsRole.role == Role.DRIVER ? Role.PASSENGER : Role.DRIVER, role: adsRole.role == Role.DRIVER ? Role.PASSENGER : Role.DRIVER,
dateInterval: {
lowerDate: this._maxDateString(
this.query.fromDate,
adEntity.getProps().fromDate,
),
higherDate: this._minDateString(
this.query.toDate,
adEntity.getProps().toDate,
),
},
driverWaypoints: driverWaypoints:
adsRole.role == Role.PASSENGER adsRole.role == Role.PASSENGER
? adEntity.getProps().waypoints ? adEntity.getProps().waypoints
@ -173,7 +183,7 @@ export class PassengerOrientedSelector extends Selector {
scheduleDates.map((date: Date) => { scheduleDates.map((date: Date) => {
this.query.schedule this.query.schedule
.filter( .filter(
(scheduleItem: ScheduleItem) => date.getDay() == scheduleItem.day, (scheduleItem: ScheduleItem) => date.getUTCDay() == scheduleItem.day,
) )
.map((scheduleItem: ScheduleItem) => { .map((scheduleItem: ScheduleItem) => {
switch (role) { switch (role) {
@ -205,15 +215,15 @@ export class PassengerOrientedSelector extends Selector {
); );
// we want the min departure time of the driver to be before the max departure time of the passenger // we want the min departure time of the driver to be before the max departure time of the passenger
return `make_timestamp(\ return `make_timestamp(\
${maxDepartureDatetime.getFullYear()},\ ${maxDepartureDatetime.getUTCFullYear()},\
${maxDepartureDatetime.getMonth() + 1},\ ${maxDepartureDatetime.getUTCMonth() + 1},\
${maxDepartureDatetime.getDate()},\ ${maxDepartureDatetime.getUTCDate()},\
CAST(EXTRACT(hour from time) as integer),\ CAST(EXTRACT(hour from time) as integer),\
CAST(EXTRACT(minute from time) as integer),0) - interval '1 second' * margin <=\ CAST(EXTRACT(minute from time) as integer),0) - interval '1 second' * margin <=\
make_timestamp(\ make_timestamp(\
${maxDepartureDatetime.getFullYear()},\ ${maxDepartureDatetime.getUTCFullYear()},\
${maxDepartureDatetime.getMonth() + 1},\ ${maxDepartureDatetime.getUTCMonth() + 1},\
${maxDepartureDatetime.getDate()},${maxDepartureDatetime.getHours()},${maxDepartureDatetime.getMinutes()},0)`; ${maxDepartureDatetime.getUTCDate()},${maxDepartureDatetime.getUTCHours()},${maxDepartureDatetime.getUTCMinutes()},0)`;
}; };
private _whereDriverSchedule = ( private _whereDriverSchedule = (
@ -229,15 +239,15 @@ export class PassengerOrientedSelector extends Selector {
); );
// we want the max departure time of the passenger to be after the min departure time of the driver // we want the max departure time of the passenger to be after the min departure time of the driver
return `make_timestamp(\ return `make_timestamp(\
${minDepartureDatetime.getFullYear()}, ${minDepartureDatetime.getUTCFullYear()},
${minDepartureDatetime.getMonth() + 1}, ${minDepartureDatetime.getUTCMonth() + 1},
${minDepartureDatetime.getDate()},\ ${minDepartureDatetime.getUTCDate()},\
CAST(EXTRACT(hour from time) as integer),\ CAST(EXTRACT(hour from time) as integer),\
CAST(EXTRACT(minute from time) as integer),0) + interval '1 second' * margin >=\ CAST(EXTRACT(minute from time) as integer),0) + interval '1 second' * margin >=\
make_timestamp(\ make_timestamp(\
${minDepartureDatetime.getFullYear()}, ${minDepartureDatetime.getUTCFullYear()},
${minDepartureDatetime.getMonth() + 1}, ${minDepartureDatetime.getUTCMonth() + 1},
${minDepartureDatetime.getDate()},${minDepartureDatetime.getHours()},${minDepartureDatetime.getMinutes()},0)`; ${minDepartureDatetime.getUTCDate()},${minDepartureDatetime.getUTCHours()},${minDepartureDatetime.getUTCMinutes()},0)`;
}; };
private _whereAzimuth = (): string => { private _whereAzimuth = (): string => {
@ -311,7 +321,7 @@ export class PassengerOrientedSelector extends Selector {
for ( for (
let date = fromDate; let date = fromDate;
date <= toDate; date <= toDate;
date.setDate(date.getDate() + 1) date.setUTCDate(date.getUTCDate() + 1)
) { ) {
dates.push(new Date(date)); dates.push(new Date(date));
count++; count++;
@ -321,7 +331,7 @@ export class PassengerOrientedSelector extends Selector {
}; };
private _addMargin = (date: Date, marginInSeconds: number): Date => { private _addMargin = (date: Date, marginInSeconds: number): Date => {
date.setTime(date.getTime() + marginInSeconds * 1000); date.setUTCSeconds(marginInSeconds);
return date; return date;
}; };
@ -334,6 +344,12 @@ export class PassengerOrientedSelector extends Selector {
maxAzimuth: maxAzimuth:
azimuth + margin > 360 ? azimuth + margin - 360 : azimuth + margin, azimuth + margin > 360 ? azimuth + margin - 360 : azimuth + margin,
}); });
private _maxDateString = (date1: string, date2: string): string =>
new Date(date1) > new Date(date2) ? date1 : date2;
private _minDateString = (date1: string, date2: string): string =>
new Date(date1) < new Date(date2) ? date1 : date2;
} }
export type QueryStringRole = { export type QueryStringRole = {

View File

@ -1,31 +1,29 @@
import { ExceptionBase } from '@mobicoop/ddd-library'; import { ExceptionBase } from '@mobicoop/ddd-library';
import { DateInterval } from './candidate.types';
export class CalendarTools { export class CalendarTools {
/** /**
* Returns the first date corresponding to a week day (0 based monday) * Returns the first date corresponding to a week day (0 based monday)
* within a date range * within a date range
*/ */
static firstDate = ( static firstDate = (weekDay: number, dateInterval: DateInterval): Date => {
weekDay: number,
lowerDate: string,
higherDate: string,
): Date => {
if (weekDay < 0 || weekDay > 6) if (weekDay < 0 || weekDay > 6)
throw new CalendarToolsException( throw new CalendarToolsException(
new Error('weekDay must be between 0 and 6'), new Error('weekDay must be between 0 and 6'),
); );
const lowerDateAsDate: Date = new Date(lowerDate); const lowerDateAsDate: Date = new Date(dateInterval.lowerDate);
const higherDateAsDate: Date = new Date(higherDate); const higherDateAsDate: Date = new Date(dateInterval.higherDate);
if (lowerDateAsDate.getDay() == weekDay) return lowerDateAsDate; if (lowerDateAsDate.getUTCDay() == weekDay) return lowerDateAsDate;
const nextDate: Date = new Date(lowerDateAsDate); const nextDate: Date = new Date(lowerDateAsDate);
nextDate.setDate( nextDate.setUTCDate(
lowerDateAsDate.getDate() + (7 - (lowerDateAsDate.getDay() - weekDay)), lowerDateAsDate.getUTCDate() +
(7 - (lowerDateAsDate.getUTCDay() - weekDay)),
); );
if (lowerDateAsDate.getDay() < weekDay) { if (lowerDateAsDate.getUTCDay() < weekDay) {
nextDate.setMonth(lowerDateAsDate.getMonth()); nextDate.setUTCMonth(lowerDateAsDate.getUTCMonth());
nextDate.setFullYear(lowerDateAsDate.getFullYear()); nextDate.setUTCFullYear(lowerDateAsDate.getUTCFullYear());
nextDate.setDate( nextDate.setUTCDate(
lowerDateAsDate.getDate() + (weekDay - lowerDateAsDate.getDay()), lowerDateAsDate.getUTCDate() + (weekDay - lowerDateAsDate.getUTCDay()),
); );
} }
if (nextDate <= higherDateAsDate) return nextDate; if (nextDate <= higherDateAsDate) return nextDate;
@ -38,28 +36,24 @@ export class CalendarTools {
* Returns the last date corresponding to a week day (0 based monday) * Returns the last date corresponding to a week day (0 based monday)
* within a date range * within a date range
*/ */
static lastDate = ( static lastDate = (weekDay: number, dateInterval: DateInterval): Date => {
weekDay: number,
lowerDate: string,
higherDate: string,
): Date => {
if (weekDay < 0 || weekDay > 6) if (weekDay < 0 || weekDay > 6)
throw new CalendarToolsException( throw new CalendarToolsException(
new Error('weekDay must be between 0 and 6'), new Error('weekDay must be between 0 and 6'),
); );
const lowerDateAsDate: Date = new Date(lowerDate); const lowerDateAsDate: Date = new Date(dateInterval.lowerDate);
const higherDateAsDate: Date = new Date(higherDate); const higherDateAsDate: Date = new Date(dateInterval.higherDate);
if (higherDateAsDate.getDay() == weekDay) return higherDateAsDate; if (higherDateAsDate.getUTCDay() == weekDay) return higherDateAsDate;
const previousDate: Date = new Date(higherDateAsDate); const previousDate: Date = new Date(higherDateAsDate);
previousDate.setDate( previousDate.setUTCDate(
higherDateAsDate.getDate() - (higherDateAsDate.getDay() - weekDay), higherDateAsDate.getUTCDate() - (higherDateAsDate.getUTCDay() - weekDay),
); );
if (higherDateAsDate.getDay() < weekDay) { if (higherDateAsDate.getUTCDay() < weekDay) {
previousDate.setMonth(higherDateAsDate.getMonth()); previousDate.setUTCMonth(higherDateAsDate.getUTCMonth());
previousDate.setFullYear(higherDateAsDate.getFullYear()); previousDate.setUTCFullYear(higherDateAsDate.getUTCFullYear());
previousDate.setDate( previousDate.setUTCDate(
higherDateAsDate.getDate() - higherDateAsDate.getUTCDate() -
(7 + (higherDateAsDate.getDay() - weekDay)), (7 + (higherDateAsDate.getUTCDay() - weekDay)),
); );
} }
if (previousDate >= lowerDateAsDate) return previousDate; if (previousDate >= lowerDateAsDate) return previousDate;
@ -67,6 +61,55 @@ export class CalendarTools {
new Error('no available day for the given date range'), new Error('no available day for the given date range'),
); );
}; };
/**
* Returns a date from a date and time as strings, adding optional seconds
*/
static datetimeFromString = (
date: string,
time: string,
additionalSeconds = 0,
): Date => {
const datetime = new Date(`${date}T${time}:00Z`);
datetime.setUTCSeconds(additionalSeconds);
return datetime;
};
/**
* 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
*/
static epochDaysFromTime = (weekDay: number, time: string): Date[] => {
if (weekDay < 0 || weekDay > 6)
throw new CalendarToolsException(
new Error('weekDay must be between 0 and 6'),
);
switch (weekDay) {
case 0:
return [
new Date(`1969-12-28T${time}:00Z`),
new Date(`1970-01-04T${time}:00Z`),
];
case 1:
return [new Date(`1969-12-29T${time}:00Z`)];
case 2:
return [new Date(`1969-12-30T${time}:00Z`)];
case 3:
return [new Date(`1969-12-31T${time}:00Z`)];
case 5:
return [new Date(`1970-01-02T${time}:00Z`)];
case 6:
return [
new Date(`1969-12-27T${time}:00Z`),
new Date(`1970-01-03T${time}:00Z`),
];
case 4:
default:
return [new Date(`1970-01-01T${time}:00Z`)];
}
};
} }
export class CalendarToolsException extends ExceptionBase { export class CalendarToolsException extends ExceptionBase {

View File

@ -1,11 +1,21 @@
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import { CandidateProps, CreateCandidateProps } from './candidate.types'; import {
import { CarpoolStepProps } from './value-objects/carpool-step.value-object'; CandidateProps,
import { StepProps } from './value-objects/step.value-object'; CreateCandidateProps,
Target,
} from './candidate.types';
import {
CarpoolPathItem,
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 } from './value-objects/schedule-item.value-object';
import { Journey } from './value-objects/journey.value-object'; import { Journey } from './value-objects/journey.value-object';
import { CalendarTools } from './calendar-tools.service'; import { CalendarTools } from './calendar-tools.service';
import { JourneyItem } from './value-objects/journey-item.value-object'; import { JourneyItem } from './value-objects/journey-item.value-object';
import { Actor } from './value-objects/actor.value-object';
import { ActorTime } from './value-objects/actor-time.value-object';
import { Role } from './ad.types';
export class CandidateEntity extends AggregateRoot<CandidateProps> { export class CandidateEntity extends AggregateRoot<CandidateProps> {
protected readonly _id: AggregateID; protected readonly _id: AggregateID;
@ -15,8 +25,8 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
return new CandidateEntity({ id: create.id, props }); return new CandidateEntity({ id: create.id, props });
}; };
setCarpoolPath = (carpoolSteps: CarpoolStepProps[]): CandidateEntity => { setCarpoolPath = (carpoolPath: CarpoolPathItemProps[]): CandidateEntity => {
this.props.carpoolSteps = carpoolSteps; this.props.carpoolPath = carpoolPath;
return this; return this;
}; };
@ -34,21 +44,14 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
isDetourValid = (): boolean => isDetourValid = (): boolean =>
this._validateDistanceDetour() && this._validateDurationDetour(); this._validateDistanceDetour() && this._validateDurationDetour();
createJourneys = (fromDate: string, toDate: string): CandidateEntity => { /**
this.props.driverJourneys = this.props.driverSchedule * Create the journeys based on the driver schedule (the driver 'drives' the carpool !)
.map((driverScheduleItem: ScheduleItem) => */
this._createJourney(fromDate, toDate, driverScheduleItem), createJourneys = (): CandidateEntity => {
) this.props.journeys = this.props.driverSchedule.map(
.filter( (driverScheduleItem: ScheduleItem) =>
(journey: Journey | undefined) => journey !== undefined, this._createJourney(driverScheduleItem),
) as Journey[]; );
this.props.passengerJourneys = this.props.passengerSchedule
.map((passengerScheduleItem: ScheduleItem) =>
this._createJourney(fromDate, toDate, passengerScheduleItem),
)
.filter(
(journey: Journey | undefined) => journey !== undefined,
) as Journey[];
return this; return this;
}; };
@ -66,23 +69,159 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
(1 + this.props.spacetimeDetourRatio.maxDistanceDetourRatio) (1 + this.props.spacetimeDetourRatio.maxDistanceDetourRatio)
: false; : false;
private _createJourney = ( private _createJourney = (driverScheduleItem: ScheduleItem): Journey =>
fromDate: string,
toDate: string,
scheduleItem: ScheduleItem,
): Journey | undefined =>
new Journey({ new Journey({
day: scheduleItem.day, firstDate: CalendarTools.firstDate(
firstDate: CalendarTools.firstDate(scheduleItem.day, fromDate, toDate), driverScheduleItem.day,
lastDate: CalendarTools.lastDate(scheduleItem.day, fromDate, toDate), this.props.dateInterval,
journeyItems: this._createJourneyItems(scheduleItem), ),
lastDate: CalendarTools.lastDate(
driverScheduleItem.day,
this.props.dateInterval,
),
journeyItems: this._createJourneyItems(driverScheduleItem),
}); });
private _createJourneyItems = ( private _createJourneyItems = (
scheduleItem: ScheduleItem, driverScheduleItem: ScheduleItem,
): JourneyItem[] => []; ): JourneyItem[] =>
this.props.carpoolPath?.map(
(carpoolPathItem: CarpoolPathItem, index: number) =>
this._createJourneyItem(carpoolPathItem, index, driverScheduleItem),
) as JourneyItem[];
private _createJourneyItem = (
carpoolPathItem: CarpoolPathItem,
stepIndex: number,
driverScheduleItem: ScheduleItem,
): JourneyItem =>
new JourneyItem({
lon: carpoolPathItem.lon,
lat: carpoolPathItem.lat,
duration: ((this.props.steps as Step[])[stepIndex] as Step).duration,
distance: ((this.props.steps as Step[])[stepIndex] as Step).distance,
actorTimes: carpoolPathItem.actors.map((actor: Actor) =>
this._createActorTime(
actor,
driverScheduleItem,
((this.props.steps as Step[])[stepIndex] as Step).duration,
),
),
});
private _createActorTime = (
actor: Actor,
driverScheduleItem: ScheduleItem,
duration: number,
): ActorTime => {
const scheduleItem: ScheduleItem =
actor.role == Role.PASSENGER && actor.target == Target.START
? this._closestPassengerScheduleItem(driverScheduleItem)
: driverScheduleItem;
const effectiveDuration =
(actor.role == Role.PASSENGER && actor.target == Target.START) ||
actor.target == Target.START
? 0
: duration;
return new ActorTime({
role: actor.role,
target: actor.target,
firstDatetime: CalendarTools.datetimeFromString(
this.props.dateInterval.lowerDate,
scheduleItem.time,
effectiveDuration,
),
firstMinDatetime: CalendarTools.datetimeFromString(
this.props.dateInterval.lowerDate,
scheduleItem.time,
-scheduleItem.margin + effectiveDuration,
),
firstMaxDatetime: CalendarTools.datetimeFromString(
this.props.dateInterval.lowerDate,
scheduleItem.time,
scheduleItem.margin + effectiveDuration,
),
lastDatetime: CalendarTools.datetimeFromString(
this.props.dateInterval.higherDate,
scheduleItem.time,
effectiveDuration,
),
lastMinDatetime: CalendarTools.datetimeFromString(
this.props.dateInterval.higherDate,
scheduleItem.time,
-scheduleItem.margin + effectiveDuration,
),
lastMaxDatetime: CalendarTools.datetimeFromString(
this.props.dateInterval.higherDate,
scheduleItem.time,
scheduleItem.margin + effectiveDuration,
),
});
};
private _closestPassengerScheduleItem = (
driverScheduleItem: ScheduleItem,
): ScheduleItem =>
CalendarTools.epochDaysFromTime(
driverScheduleItem.day,
driverScheduleItem.time,
)
.map((driverDate: Date) =>
this._minPassengerScheduleItemGapForDate(driverDate),
)
.reduce(
(
previousScheduleItemGap: ScheduleItemGap,
currentScheduleItemGap: ScheduleItemGap,
) =>
previousScheduleItemGap.gap < currentScheduleItemGap.gap
? previousScheduleItemGap
: currentScheduleItemGap,
).scheduleItem;
private _minPassengerScheduleItemGapForDate = (date: Date): ScheduleItemGap =>
this.props.passengerSchedule
.map(
(scheduleItem: ScheduleItem) =>
<ScheduleItemRange>{
scheduleItem,
range: CalendarTools.epochDaysFromTime(
scheduleItem.day,
scheduleItem.time,
),
},
)
.map((scheduleItemRange: ScheduleItemRange) => ({
scheduleItem: scheduleItemRange.scheduleItem,
gap: scheduleItemRange.range
.map((scheduleDate: Date) =>
Math.round(Math.abs(scheduleDate.getTime() - date.getTime())),
)
.reduce((previousGap: number, currentGap: number) =>
previousGap < currentGap ? previousGap : currentGap,
),
}))
.reduce(
(
previousScheduleItemGap: ScheduleItemGap,
currentScheduleItemGap: ScheduleItemGap,
) =>
previousScheduleItemGap.gap < currentScheduleItemGap.gap
? previousScheduleItemGap
: currentScheduleItemGap,
);
validate(): void { validate(): void {
// entity business rules validation to protect it's invariant before saving entity to a database // entity business rules validation to protect it's invariant before saving entity to a database
} }
} }
type ScheduleItemRange = {
scheduleItem: ScheduleItem;
range: Date[];
};
type ScheduleItemGap = {
scheduleItem: ScheduleItem;
gap: number;
};

View File

@ -1,7 +1,7 @@
import { Role } from './ad.types'; import { Role } from './ad.types';
import { PointProps } from './value-objects/point.value-object'; import { PointProps } from './value-objects/point.value-object';
import { ScheduleItemProps } from './value-objects/schedule-item.value-object'; import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
import { CarpoolStepProps } from './value-objects/carpool-step.value-object'; import { CarpoolPathItemProps } from './value-objects/carpool-path-item.value-object';
import { JourneyProps } from './value-objects/journey.value-object'; import { JourneyProps } from './value-objects/journey.value-object';
import { StepProps } from './value-objects/step.value-object'; import { StepProps } from './value-objects/step.value-object';
@ -10,16 +10,16 @@ export interface CandidateProps {
role: Role; role: Role;
driverWaypoints: PointProps[]; driverWaypoints: PointProps[];
passengerWaypoints: PointProps[]; passengerWaypoints: PointProps[];
driverSchedule: ScheduleItemProps[];
passengerSchedule: ScheduleItemProps[];
driverDistance: number; driverDistance: number;
driverDuration: number; driverDuration: number;
carpoolSteps?: CarpoolStepProps[]; dateInterval: DateInterval;
carpoolPath?: CarpoolPathItemProps[];
distance?: number; distance?: number;
duration?: number; duration?: number;
steps?: StepProps[]; steps?: StepProps[];
driverSchedule: ScheduleItemProps[]; journeys?: JourneyProps[];
passengerSchedule: ScheduleItemProps[];
driverJourneys?: JourneyProps[];
passengerJourneys?: JourneyProps[];
spacetimeDetourRatio: SpacetimeDetourRatio; spacetimeDetourRatio: SpacetimeDetourRatio;
} }
@ -34,6 +34,7 @@ export interface CreateCandidateProps {
driverSchedule: ScheduleItemProps[]; driverSchedule: ScheduleItemProps[];
passengerSchedule: ScheduleItemProps[]; passengerSchedule: ScheduleItemProps[];
spacetimeDetourRatio: SpacetimeDetourRatio; spacetimeDetourRatio: SpacetimeDetourRatio;
dateInterval: DateInterval;
} }
export enum Target { export enum Target {
@ -47,12 +48,12 @@ export abstract class Validator {
abstract validate(): boolean; abstract validate(): boolean;
} }
export type SpacetimeMetric = {
distance: number;
duration: number;
};
export type SpacetimeDetourRatio = { export type SpacetimeDetourRatio = {
maxDistanceDetourRatio: number; maxDistanceDetourRatio: number;
maxDurationDetourRatio: number; maxDurationDetourRatio: number;
}; };
export type DateInterval = {
lowerDate: string;
higherDate: string;
};

View File

@ -3,7 +3,7 @@ import { Target } from './candidate.types';
import { CarpoolPathCreatorException } from './match.errors'; import { CarpoolPathCreatorException } from './match.errors';
import { Actor } from './value-objects/actor.value-object'; import { Actor } from './value-objects/actor.value-object';
import { Point } from './value-objects/point.value-object'; import { Point } from './value-objects/point.value-object';
import { CarpoolStep } from './value-objects/carpool-step.value-object'; import { CarpoolPathItem } from './value-objects/carpool-path-item.value-object';
export class CarpoolPathCreator { export class CarpoolPathCreator {
private PRECISION = 5; private PRECISION = 5;
@ -23,39 +23,34 @@ export class CarpoolPathCreator {
} }
/** /**
* Creates a path (a list of carpoolSteps) between driver waypoints * Creates a path (a list of carpoolPathItem) between driver waypoints
and passenger waypoints respecting the order and passenger waypoints respecting the order
of the driver waypoints of the driver waypoints
Inspired by : Inspired by :
https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
*/ */
public carpoolPath = (): CarpoolStep[] => public carpoolPath = (): CarpoolPathItem[] =>
this._consolidate( this._consolidate(
this._mixedCarpoolSteps( this._mixedCarpoolPath(
this._driverCarpoolSteps(), this._driverCarpoolPath(),
this._passengerCarpoolSteps(), this._passengerCarpoolPath(),
), ),
); );
private _mixedCarpoolSteps = ( private _mixedCarpoolPath = (
driverCarpoolSteps: CarpoolStep[], driverCarpoolPath: CarpoolPathItem[],
passengerCarpoolSteps: CarpoolStep[], passengerCarpoolPath: CarpoolPathItem[],
): CarpoolStep[] => ): CarpoolPathItem[] =>
driverCarpoolSteps.length == 2 driverCarpoolPath.length == 2
? this._simpleMixedCarpoolSteps(driverCarpoolSteps, passengerCarpoolSteps) ? this._simpleMixedCarpoolPath(driverCarpoolPath, passengerCarpoolPath)
: this._complexMixedCarpoolSteps( : this._complexMixedCarpoolPath(driverCarpoolPath, passengerCarpoolPath);
driverCarpoolSteps,
passengerCarpoolSteps,
);
private _driverCarpoolSteps = (): CarpoolStep[] => private _driverCarpoolPath = (): CarpoolPathItem[] =>
this.driverWaypoints.map( this.driverWaypoints.map(
(waypoint: Point, index: number) => (waypoint: Point, index: number) =>
new CarpoolStep({ new CarpoolPathItem({
point: new Point({
lon: waypoint.lon, lon: waypoint.lon,
lat: waypoint.lat, lat: waypoint.lat,
}),
actors: [ actors: [
new Actor({ new Actor({
role: Role.DRIVER, role: Role.DRIVER,
@ -66,17 +61,15 @@ export class CarpoolPathCreator {
); );
/** /**
* Creates the passenger carpoolSteps with original passenger waypoints, adding driver waypoints that are the same * Creates the passenger carpoolPath with original passenger waypoints, adding driver waypoints that are the same
*/ */
private _passengerCarpoolSteps = (): CarpoolStep[] => { private _passengerCarpoolPath = (): CarpoolPathItem[] => {
const carpoolSteps: CarpoolStep[] = []; const carpoolPath: CarpoolPathItem[] = [];
this.passengerWaypoints.forEach( this.passengerWaypoints.forEach(
(passengerWaypoint: Point, index: number) => { (passengerWaypoint: Point, index: number) => {
const carpoolStep: CarpoolStep = new CarpoolStep({ const carpoolPathItem: CarpoolPathItem = new CarpoolPathItem({
point: new Point({
lon: passengerWaypoint.lon, lon: passengerWaypoint.lon,
lat: passengerWaypoint.lat, lat: passengerWaypoint.lat,
}),
actors: [ actors: [
new Actor({ new Actor({
role: Role.PASSENGER, role: Role.PASSENGER,
@ -89,78 +82,80 @@ export class CarpoolPathCreator {
passengerWaypoint.equals(driverWaypoint), passengerWaypoint.equals(driverWaypoint),
).length == 0 ).length == 0
) { ) {
carpoolStep.actors.push( carpoolPathItem.actors.push(
new Actor({ new Actor({
role: Role.DRIVER, role: Role.DRIVER,
target: Target.NEUTRAL, target: Target.NEUTRAL,
}), }),
); );
} }
carpoolSteps.push(carpoolStep); carpoolPath.push(carpoolPathItem);
}, },
); );
return carpoolSteps; return carpoolPath;
}; };
private _simpleMixedCarpoolSteps = ( private _simpleMixedCarpoolPath = (
driverCarpoolSteps: CarpoolStep[], driverCarpoolPath: CarpoolPathItem[],
passengerCarpoolSteps: CarpoolStep[], passengerCarpoolPath: CarpoolPathItem[],
): CarpoolStep[] => [ ): CarpoolPathItem[] => [
driverCarpoolSteps[0], driverCarpoolPath[0],
...passengerCarpoolSteps, ...passengerCarpoolPath,
driverCarpoolSteps[1], driverCarpoolPath[1],
]; ];
private _complexMixedCarpoolSteps = ( private _complexMixedCarpoolPath = (
driverCarpoolSteps: CarpoolStep[], driverCarpoolPath: CarpoolPathItem[],
passengerCarpoolSteps: CarpoolStep[], passengerCarpoolPath: CarpoolPathItem[],
): CarpoolStep[] => { ): CarpoolPathItem[] => {
let mixedCarpoolSteps: CarpoolStep[] = [...driverCarpoolSteps]; let mixedCarpoolPath: CarpoolPathItem[] = [...driverCarpoolPath];
const originInsertIndex: number = this._insertIndex( const originInsertIndex: number = this._insertIndex(
passengerCarpoolSteps[0], passengerCarpoolPath[0],
driverCarpoolSteps, driverCarpoolPath,
); );
mixedCarpoolSteps = [ mixedCarpoolPath = [
...mixedCarpoolSteps.slice(0, originInsertIndex), ...mixedCarpoolPath.slice(0, originInsertIndex),
passengerCarpoolSteps[0], passengerCarpoolPath[0],
...mixedCarpoolSteps.slice(originInsertIndex), ...mixedCarpoolPath.slice(originInsertIndex),
]; ];
const destinationInsertIndex: number = const destinationInsertIndex: number =
this._insertIndex( this._insertIndex(
passengerCarpoolSteps[passengerCarpoolSteps.length - 1], passengerCarpoolPath[passengerCarpoolPath.length - 1],
driverCarpoolSteps, driverCarpoolPath,
) + 1; ) + 1;
mixedCarpoolSteps = [ mixedCarpoolPath = [
...mixedCarpoolSteps.slice(0, destinationInsertIndex), ...mixedCarpoolPath.slice(0, destinationInsertIndex),
passengerCarpoolSteps[passengerCarpoolSteps.length - 1], passengerCarpoolPath[passengerCarpoolPath.length - 1],
...mixedCarpoolSteps.slice(destinationInsertIndex), ...mixedCarpoolPath.slice(destinationInsertIndex),
]; ];
return mixedCarpoolSteps; return mixedCarpoolPath;
}; };
private _insertIndex = ( private _insertIndex = (
targetCarpoolStep: CarpoolStep, targetCarpoolPathItem: CarpoolPathItem,
carpoolSteps: CarpoolStep[], carpoolPath: CarpoolPathItem[],
): number => ): number =>
this._closestSegmentIndex(targetCarpoolStep, this._segments(carpoolSteps)) + this._closestSegmentIndex(
1; targetCarpoolPathItem,
this._segments(carpoolPath),
) + 1;
private _segments = (carpoolSteps: CarpoolStep[]): CarpoolStep[][] => { private _segments = (carpoolPath: CarpoolPathItem[]): CarpoolPathItem[][] => {
const segments: CarpoolStep[][] = []; const segments: CarpoolPathItem[][] = [];
carpoolSteps.forEach((carpoolStep: CarpoolStep, index: number) => { carpoolPath.forEach((carpoolPathItem: CarpoolPathItem, index: number) => {
if (index < carpoolSteps.length - 1) if (index < carpoolPath.length - 1)
segments.push([carpoolStep, carpoolSteps[index + 1]]); segments.push([carpoolPathItem, carpoolPath[index + 1]]);
}); });
return segments; return segments;
}; };
private _closestSegmentIndex = ( private _closestSegmentIndex = (
carpoolStep: CarpoolStep, carpoolPathItem: CarpoolPathItem,
segments: CarpoolStep[][], segments: CarpoolPathItem[][],
): number => { ): number => {
const distances: Map<number, number> = new Map(); const distances: Map<number, number> = new Map();
segments.forEach((segment: CarpoolStep[], index: number) => { segments.forEach((segment: CarpoolPathItem[], index: number) => {
distances.set(index, this._distanceToSegment(carpoolStep, segment)); distances.set(index, this._distanceToSegment(carpoolPathItem, segment));
}); });
const sortedDistances: Map<number, number> = new Map( const sortedDistances: Map<number, number> = new Map(
[...distances.entries()].sort((a, b) => a[1] - b[1]), [...distances.entries()].sort((a, b) => a[1] - b[1]),
@ -170,45 +165,62 @@ export class CarpoolPathCreator {
}; };
private _distanceToSegment = ( private _distanceToSegment = (
carpoolStep: CarpoolStep, carpoolPathItem: CarpoolPathItem,
segment: CarpoolStep[], segment: CarpoolPathItem[],
): number => ): number =>
parseFloat( parseFloat(
Math.sqrt(this._distanceToSegmentSquared(carpoolStep, segment)).toFixed( Math.sqrt(
this.PRECISION, this._distanceToSegmentSquared(carpoolPathItem, segment),
), ).toFixed(this.PRECISION),
); );
private _distanceToSegmentSquared = ( private _distanceToSegmentSquared = (
carpoolStep: CarpoolStep, carpoolPathItem: CarpoolPathItem,
segment: CarpoolStep[], segment: CarpoolPathItem[],
): number => { ): number => {
const length2: number = this._distanceSquared( const length2: number = this._distanceSquared(
segment[0].point, new Point({
segment[1].point, lon: segment[0].lon,
lat: segment[0].lat,
}),
new Point({
lon: segment[1].lon,
lat: segment[1].lat,
}),
); );
if (length2 == 0) if (length2 == 0)
return this._distanceSquared(carpoolStep.point, segment[0].point); return this._distanceSquared(
new Point({
lon: carpoolPathItem.lon,
lat: carpoolPathItem.lat,
}),
new Point({
lon: segment[0].lon,
lat: segment[0].lat,
}),
);
const length: number = Math.max( const length: number = Math.max(
0, 0,
Math.min( Math.min(
1, 1,
((carpoolStep.point.lon - segment[0].point.lon) * ((carpoolPathItem.lon - segment[0].lon) *
(segment[1].point.lon - segment[0].point.lon) + (segment[1].lon - segment[0].lon) +
(carpoolStep.point.lat - segment[0].point.lat) * (carpoolPathItem.lat - segment[0].lat) *
(segment[1].point.lat - segment[0].point.lat)) / (segment[1].lat - segment[0].lat)) /
length2, length2,
), ),
); );
const newPoint: Point = new Point({ const newPoint: Point = new Point({
lon: lon: segment[0].lon + length * (segment[1].lon - segment[0].lon),
segment[0].point.lon + lat: segment[0].lat + length * (segment[1].lat - segment[0].lat),
length * (segment[1].point.lon - segment[0].point.lon),
lat:
segment[0].point.lat +
length * (segment[1].point.lat - segment[0].point.lat),
}); });
return this._distanceSquared(carpoolStep.point, newPoint); return this._distanceSquared(
new Point({
lon: carpoolPathItem.lon,
lat: carpoolPathItem.lat,
}),
newPoint,
);
}; };
private _distanceSquared = (point1: Point, point2: Point): number => private _distanceSquared = (point1: Point, point2: Point): number =>
@ -227,31 +239,43 @@ export class CarpoolPathCreator {
: Target.INTERMEDIATE; : Target.INTERMEDIATE;
/** /**
* Consolidate carpoolSteps by removing duplicate actors (eg. driver with neutral and start or finish target) * Consolidate carpoolPath by removing duplicate actors (eg. driver with neutral and start or finish target)
*/ */
private _consolidate = (carpoolSteps: CarpoolStep[]): CarpoolStep[] => { private _consolidate = (
carpoolPath: CarpoolPathItem[],
): CarpoolPathItem[] => {
const uniquePoints: Point[] = []; const uniquePoints: Point[] = [];
carpoolSteps.forEach((carpoolStep: CarpoolStep) => { carpoolPath.forEach((carpoolPathItem: CarpoolPathItem) => {
if ( if (
uniquePoints.find((point: Point) => point.equals(carpoolStep.point)) === uniquePoints.find((point: Point) =>
undefined point.equals(
new Point({
lon: carpoolPathItem.lon,
lat: carpoolPathItem.lat,
}),
),
) === undefined
) )
uniquePoints.push( uniquePoints.push(
new Point({ new Point({
lon: carpoolStep.point.lon, lon: carpoolPathItem.lon,
lat: carpoolStep.point.lat, lat: carpoolPathItem.lat,
}), }),
); );
}); });
return uniquePoints.map( return uniquePoints.map(
(point: Point) => (point: Point) =>
new CarpoolStep({ new CarpoolPathItem({
point, lon: point.lon,
actors: carpoolSteps lat: point.lat,
.filter((carpoolStep: CarpoolStep) => actors: carpoolPath
carpoolStep.point.equals(point), .filter((carpoolPathItem: CarpoolPathItem) =>
new Point({
lon: carpoolPathItem.lon,
lat: carpoolPathItem.lat,
}).equals(point),
) )
.map((carpoolStep: CarpoolStep) => carpoolStep.actors) .map((carpoolPathItem: CarpoolPathItem) => carpoolPathItem.actors)
.flat(), .flat(),
}), }),
); );

View File

@ -56,7 +56,7 @@ export class ActorTime extends ValueObject<ActorTimeProps> {
role: props.role, role: props.role,
target: props.target, target: props.target,
}); });
if (props.firstDatetime.getDay() != props.lastDatetime.getDay()) if (props.firstDatetime.getUTCDay() != props.lastDatetime.getUTCDay())
throw new ArgumentInvalidException( throw new ArgumentInvalidException(
'firstDatetime week day must be equal to lastDatetime week day', 'firstDatetime week day must be equal to lastDatetime week day',
); );

View File

@ -4,28 +4,36 @@ import {
} from '@mobicoop/ddd-library'; } from '@mobicoop/ddd-library';
import { Actor } from './actor.value-object'; import { Actor } from './actor.value-object';
import { Role } from '../ad.types'; import { Role } from '../ad.types';
import { Point } from './point.value-object'; import { Point, PointProps } from './point.value-object';
/** Note: /** Note:
* Value Objects with multiple properties can contain * Value Objects with multiple properties can contain
* other Value Objects inside if needed. * other Value Objects inside if needed.
* */ * */
export interface CarpoolStepProps { export interface CarpoolPathItemProps extends PointProps {
point: Point;
actors: Actor[]; actors: Actor[];
} }
export class CarpoolStep extends ValueObject<CarpoolStepProps> { export class CarpoolPathItem extends ValueObject<CarpoolPathItemProps> {
get point(): Point { get lon(): number {
return this.props.point; return this.props.lon;
}
get lat(): number {
return this.props.lat;
} }
get actors(): Actor[] { get actors(): Actor[] {
return this.props.actors; return this.props.actors;
} }
protected validate(props: CarpoolStepProps): void { protected validate(props: CarpoolPathItemProps): void {
// validate point props
new Point({
lon: props.lon,
lat: props.lat,
});
if (props.actors.length <= 0) if (props.actors.length <= 0)
throw new ArgumentOutOfRangeException('at least one actor is required'); throw new ArgumentOutOfRangeException('at least one actor is required');
if ( if (

View File

@ -1,8 +1,4 @@
import { import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library';
ArgumentInvalidException,
ArgumentOutOfRangeException,
ValueObject,
} from '@mobicoop/ddd-library';
import { JourneyItem } from './journey-item.value-object'; import { JourneyItem } from './journey-item.value-object';
/** Note: /** Note:
@ -13,7 +9,6 @@ import { JourneyItem } from './journey-item.value-object';
export interface JourneyProps { export interface JourneyProps {
firstDate: Date; firstDate: Date;
lastDate: Date; lastDate: Date;
day: number;
journeyItems: JourneyItem[]; journeyItems: JourneyItem[];
} }
@ -26,18 +21,12 @@ export class Journey extends ValueObject<JourneyProps> {
return this.props.lastDate; return this.props.lastDate;
} }
get day(): number {
return this.props.day;
}
get journeyItems(): JourneyItem[] { get journeyItems(): JourneyItem[] {
return this.props.journeyItems; return this.props.journeyItems;
} }
protected validate(props: JourneyProps): void { protected validate(props: JourneyProps): void {
if (props.day < 0 || props.day > 6) if (props.firstDate.getUTCDay() != props.lastDate.getUTCDay())
throw new ArgumentOutOfRangeException('day must be between 0 and 6');
if (props.firstDate.getDay() != props.lastDate.getDay())
throw new ArgumentInvalidException( throw new ArgumentInvalidException(
'firstDate week day must be equal to lastDate week day', 'firstDate week day must be equal to lastDate week day',
); );

View File

@ -79,7 +79,7 @@ export class InputDateTimeTransformer implements DateTimeTransformerPort {
this._defaultTimezone, this._defaultTimezone,
)[0], )[0],
); );
return new Date(this.fromDate(geoFromDate, frequency)).getDay(); return new Date(this.fromDate(geoFromDate, frequency)).getUTCDay();
}; };
/** /**

View File

@ -45,7 +45,7 @@ export class TimeConverter implements TimeConverterPort {
.convert(TimeZone.zone('UTC')) .convert(TimeZone.zone('UTC'))
.toIsoString() .toIsoString()
.split('T')[0], .split('T')[0],
).getDay(); ).getUTCDay();
localUnixEpochDayFromTime = (time: string, timezone: string): number => localUnixEpochDayFromTime = (time: string, timezone: string): number =>
new Date( new Date(
@ -53,5 +53,5 @@ export class TimeConverter implements TimeConverterPort {
.convert(TimeZone.zone(timezone)) .convert(TimeZone.zone(timezone))
.toIsoString() .toIsoString()
.split('T')[0], .split('T')[0],
).getDay(); ).getUTCDay();
} }

View File

@ -8,21 +8,21 @@ describe('Actor time value object', () => {
const actorTimeVO = new ActorTime({ const actorTimeVO = new ActorTime({
role: Role.DRIVER, role: Role.DRIVER,
target: Target.START, target: Target.START,
firstDatetime: new Date('2023-09-01 07:00'), firstDatetime: new Date('2023-09-01T07:00Z'),
firstMinDatetime: new Date('2023-09-01 06:45'), firstMinDatetime: new Date('2023-09-01T06:45Z'),
firstMaxDatetime: new Date('2023-09-01 07:15'), firstMaxDatetime: new Date('2023-09-01T07:15Z'),
lastDatetime: new Date('2024-08-30 07:00'), lastDatetime: new Date('2024-08-30T07:00Z'),
lastMinDatetime: new Date('2024-08-30 06:45'), lastMinDatetime: new Date('2024-08-30T06:45Z'),
lastMaxDatetime: new Date('2024-08-30 07:15'), lastMaxDatetime: new Date('2024-08-30T07:15Z'),
}); });
expect(actorTimeVO.role).toBe(Role.DRIVER); expect(actorTimeVO.role).toBe(Role.DRIVER);
expect(actorTimeVO.target).toBe(Target.START); expect(actorTimeVO.target).toBe(Target.START);
expect(actorTimeVO.firstDatetime.getHours()).toBe(7); expect(actorTimeVO.firstDatetime.getUTCHours()).toBe(7);
expect(actorTimeVO.firstMinDatetime.getMinutes()).toBe(45); expect(actorTimeVO.firstMinDatetime.getUTCMinutes()).toBe(45);
expect(actorTimeVO.firstMaxDatetime.getMinutes()).toBe(15); expect(actorTimeVO.firstMaxDatetime.getUTCMinutes()).toBe(15);
expect(actorTimeVO.lastDatetime.getHours()).toBe(7); expect(actorTimeVO.lastDatetime.getUTCHours()).toBe(7);
expect(actorTimeVO.lastMinDatetime.getMinutes()).toBe(45); expect(actorTimeVO.lastMinDatetime.getUTCMinutes()).toBe(45);
expect(actorTimeVO.lastMaxDatetime.getMinutes()).toBe(15); expect(actorTimeVO.lastMaxDatetime.getUTCMinutes()).toBe(15);
}); });
it('should throw an error if dates are inconsistent', () => { it('should throw an error if dates are inconsistent', () => {
expect( expect(
@ -30,12 +30,12 @@ describe('Actor time value object', () => {
new ActorTime({ new ActorTime({
role: Role.DRIVER, role: Role.DRIVER,
target: Target.START, target: Target.START,
firstDatetime: new Date('2023-09-01 07:00'), firstDatetime: new Date('2023-09-01T07:00Z'),
firstMinDatetime: new Date('2023-09-01 07:05'), firstMinDatetime: new Date('2023-09-01T07:05Z'),
firstMaxDatetime: new Date('2023-09-01 07:15'), firstMaxDatetime: new Date('2023-09-01T07:15Z'),
lastDatetime: new Date('2024-08-30 07:00'), lastDatetime: new Date('2024-08-30T07:00Z'),
lastMinDatetime: new Date('2024-08-30 06:45'), lastMinDatetime: new Date('2024-08-30T06:45Z'),
lastMaxDatetime: new Date('2024-08-30 07:15'), lastMaxDatetime: new Date('2024-08-30T07:15Z'),
}), }),
).toThrow(ArgumentInvalidException); ).toThrow(ArgumentInvalidException);
expect( expect(
@ -43,12 +43,12 @@ describe('Actor time value object', () => {
new ActorTime({ new ActorTime({
role: Role.DRIVER, role: Role.DRIVER,
target: Target.START, target: Target.START,
firstDatetime: new Date('2023-09-01 07:00'), firstDatetime: new Date('2023-09-01T07:00Z'),
firstMinDatetime: new Date('2023-09-01 06:45'), firstMinDatetime: new Date('2023-09-01T06:45Z'),
firstMaxDatetime: new Date('2023-09-01 06:55'), firstMaxDatetime: new Date('2023-09-01T06:55Z'),
lastDatetime: new Date('2024-08-30 07:00'), lastDatetime: new Date('2024-08-30T07:00Z'),
lastMinDatetime: new Date('2024-08-30 06:45'), lastMinDatetime: new Date('2024-08-30T06:45Z'),
lastMaxDatetime: new Date('2024-08-30 07:15'), lastMaxDatetime: new Date('2024-08-30T07:15Z'),
}), }),
).toThrow(ArgumentInvalidException); ).toThrow(ArgumentInvalidException);
expect( expect(
@ -56,12 +56,12 @@ describe('Actor time value object', () => {
new ActorTime({ new ActorTime({
role: Role.DRIVER, role: Role.DRIVER,
target: Target.START, target: Target.START,
firstDatetime: new Date('2023-09-01 07:00'), firstDatetime: new Date('2023-09-01T07:00Z'),
firstMinDatetime: new Date('2023-09-01 06:45'), firstMinDatetime: new Date('2023-09-01T06:45Z'),
firstMaxDatetime: new Date('2023-09-01 07:15'), firstMaxDatetime: new Date('2023-09-01T07:15Z'),
lastDatetime: new Date('2024-08-30 07:00'), lastDatetime: new Date('2024-08-30T07:00Z'),
lastMinDatetime: new Date('2024-08-30 07:05'), lastMinDatetime: new Date('2024-08-30T07:05Z'),
lastMaxDatetime: new Date('2024-08-30 07:15'), lastMaxDatetime: new Date('2024-08-30T07:15Z'),
}), }),
).toThrow(ArgumentInvalidException); ).toThrow(ArgumentInvalidException);
expect( expect(
@ -69,12 +69,12 @@ describe('Actor time value object', () => {
new ActorTime({ new ActorTime({
role: Role.DRIVER, role: Role.DRIVER,
target: Target.START, target: Target.START,
firstDatetime: new Date('2023-09-01 07:00'), firstDatetime: new Date('2023-09-01T07:00Z'),
firstMinDatetime: new Date('2023-09-01 06:45'), firstMinDatetime: new Date('2023-09-01T06:45Z'),
firstMaxDatetime: new Date('2023-09-01 07:15'), firstMaxDatetime: new Date('2023-09-01T07:15Z'),
lastDatetime: new Date('2024-08-30 07:00'), lastDatetime: new Date('2024-08-30T07:00Z'),
lastMinDatetime: new Date('2024-08-30 06:45'), lastMinDatetime: new Date('2024-08-30T06:45Z'),
lastMaxDatetime: new Date('2024-08-30 06:35'), lastMaxDatetime: new Date('2024-08-30T06:35Z'),
}), }),
).toThrow(ArgumentInvalidException); ).toThrow(ArgumentInvalidException);
expect( expect(
@ -82,12 +82,12 @@ describe('Actor time value object', () => {
new ActorTime({ new ActorTime({
role: Role.DRIVER, role: Role.DRIVER,
target: Target.START, target: Target.START,
firstDatetime: new Date('2024-08-30 07:00'), firstDatetime: new Date('2024-08-30T07:00Z'),
firstMinDatetime: new Date('2024-08-30 06:45'), firstMinDatetime: new Date('2024-08-30T06:45Z'),
firstMaxDatetime: new Date('2024-08-30 07:15'), firstMaxDatetime: new Date('2024-08-30T07:15Z'),
lastDatetime: new Date('2023-09-01 07:00'), lastDatetime: new Date('2023-09-01T07:00Z'),
lastMinDatetime: new Date('2023-09-01 06:45'), lastMinDatetime: new Date('2023-09-01T06:45Z'),
lastMaxDatetime: new Date('2023-09-01 07:15'), lastMaxDatetime: new Date('2023-09-01T07:15Z'),
}), }),
).toThrow(ArgumentInvalidException); ).toThrow(ArgumentInvalidException);
expect( expect(
@ -95,12 +95,12 @@ describe('Actor time value object', () => {
new ActorTime({ new ActorTime({
role: Role.DRIVER, role: Role.DRIVER,
target: Target.START, target: Target.START,
firstDatetime: new Date('2023-09-01 07:00'), firstDatetime: new Date('2023-09-01T07:00Z'),
firstMinDatetime: new Date('2023-09-01 06:45'), firstMinDatetime: new Date('2023-09-01T06:45Z'),
firstMaxDatetime: new Date('2023-09-01 07:15'), firstMaxDatetime: new Date('2023-09-01T07:15Z'),
lastDatetime: new Date('2024-08-31 07:00'), lastDatetime: new Date('2024-08-31T07:00Z'),
lastMinDatetime: new Date('2024-08-31 06:45'), lastMinDatetime: new Date('2024-08-31T06:45Z'),
lastMaxDatetime: new Date('2024-08-31 06:35'), lastMaxDatetime: new Date('2024-08-31T06:35Z'),
}), }),
).toThrow(ArgumentInvalidException); ).toThrow(ArgumentInvalidException);
}); });

View File

@ -67,6 +67,10 @@ class SomeSelector extends Selector {
CandidateEntity.create({ CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
dateInterval: {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: [ driverWaypoints: [
{ {
lat: 48.678454, lat: 48.678454,

View File

@ -6,78 +6,175 @@ import {
describe('Calendar tools service', () => { describe('Calendar tools service', () => {
describe('First date', () => { describe('First date', () => {
it('should return the first date for a given week day within a date range', () => { it('should return the first date for a given week day within a date range', () => {
const firstDate: Date = CalendarTools.firstDate( const firstDate: Date = CalendarTools.firstDate(1, {
1, lowerDate: '2023-08-31',
'2023-08-31', higherDate: '2023-09-07',
'2023-09-07', });
); expect(firstDate.getUTCDay()).toBe(1);
expect(firstDate.getDay()).toBe(1); expect(firstDate.getUTCDate()).toBe(4);
expect(firstDate.getDate()).toBe(4); expect(firstDate.getUTCMonth()).toBe(8);
expect(firstDate.getMonth()).toBe(8); const secondDate: Date = CalendarTools.firstDate(5, {
const secondDate: Date = CalendarTools.firstDate( lowerDate: '2023-08-31',
5, higherDate: '2023-09-07',
'2023-08-31', });
'2023-09-07', expect(secondDate.getUTCDay()).toBe(5);
); expect(secondDate.getUTCDate()).toBe(1);
expect(secondDate.getDay()).toBe(5); expect(secondDate.getUTCMonth()).toBe(8);
expect(secondDate.getDate()).toBe(1); const thirdDate: Date = CalendarTools.firstDate(4, {
expect(secondDate.getMonth()).toBe(8); lowerDate: '2023-08-31',
const thirdDate: Date = CalendarTools.firstDate( higherDate: '2023-09-07',
4, });
'2023-08-31', expect(thirdDate.getUTCDay()).toBe(4);
'2023-09-07', expect(thirdDate.getUTCDate()).toBe(31);
); expect(thirdDate.getUTCMonth()).toBe(7);
expect(thirdDate.getDay()).toBe(4);
expect(thirdDate.getDate()).toBe(31);
expect(thirdDate.getMonth()).toBe(7);
}); });
it('should throw an exception if a given week day is not within a date range', () => { it('should throw an exception if a given week day is not within a date range', () => {
expect(() => { expect(() => {
CalendarTools.firstDate(1, '2023-09-05', '2023-09-07'); CalendarTools.firstDate(1, {
lowerDate: '2023-09-05',
higherDate: '2023-09-07',
});
}).toThrow(CalendarToolsException); }).toThrow(CalendarToolsException);
}); });
it('should throw an exception if a given week day is invalid', () => { it('should throw an exception if a given week day is invalid', () => {
expect(() => { expect(() => {
CalendarTools.firstDate(8, '2023-09-05', '2023-09-07'); CalendarTools.firstDate(8, {
lowerDate: '2023-09-05',
higherDate: '2023-09-07',
});
}).toThrow(CalendarToolsException); }).toThrow(CalendarToolsException);
}); });
}); });
describe('Second date', () => { describe('Second date', () => {
it('should return the last date for a given week day within a date range', () => { it('should return the last date for a given week day within a date range', () => {
const firstDate: Date = CalendarTools.lastDate( const firstDate: Date = CalendarTools.lastDate(0, {
0, lowerDate: '2023-09-30',
'2023-09-30', higherDate: '2024-09-30',
'2024-09-30', });
); expect(firstDate.getUTCDay()).toBe(0);
expect(firstDate.getDay()).toBe(0); expect(firstDate.getUTCDate()).toBe(29);
expect(firstDate.getDate()).toBe(29); expect(firstDate.getUTCMonth()).toBe(8);
expect(firstDate.getMonth()).toBe(8); const secondDate: Date = CalendarTools.lastDate(5, {
const secondDate: Date = CalendarTools.lastDate( lowerDate: '2023-09-30',
5, higherDate: '2024-09-30',
'2023-09-30', });
'2024-09-30', expect(secondDate.getUTCDay()).toBe(5);
); expect(secondDate.getUTCDate()).toBe(27);
expect(secondDate.getDay()).toBe(5); expect(secondDate.getUTCMonth()).toBe(8);
expect(secondDate.getDate()).toBe(27); const thirdDate: Date = CalendarTools.lastDate(1, {
expect(secondDate.getMonth()).toBe(8); lowerDate: '2023-09-30',
const thirdDate: Date = CalendarTools.lastDate( higherDate: '2024-09-30',
1, });
'2023-09-30', expect(thirdDate.getUTCDay()).toBe(1);
'2024-09-30', expect(thirdDate.getUTCDate()).toBe(30);
); expect(thirdDate.getUTCMonth()).toBe(8);
expect(thirdDate.getDay()).toBe(1);
expect(thirdDate.getDate()).toBe(30);
expect(thirdDate.getMonth()).toBe(8);
}); });
it('should throw an exception if a given week day is not within a date range', () => { it('should throw an exception if a given week day is not within a date range', () => {
expect(() => { expect(() => {
CalendarTools.lastDate(2, '2024-09-27', '2024-09-30'); CalendarTools.lastDate(2, {
lowerDate: '2024-09-27',
higherDate: '2024-09-30',
});
}).toThrow(CalendarToolsException); }).toThrow(CalendarToolsException);
}); });
it('should throw an exception if a given week day is invalid', () => { it('should throw an exception if a given week day is invalid', () => {
expect(() => { expect(() => {
CalendarTools.lastDate(8, '2023-09-30', '2024-09-30'); CalendarTools.lastDate(8, {
lowerDate: '2023-09-30',
higherDate: '2024-09-30',
});
}).toThrow(CalendarToolsException);
});
});
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',
'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',
'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',
'07:00',
-60,
);
console.log(datetime);
expect(datetime.getUTCHours()).toBe(6);
expect(datetime.getUTCMinutes()).toBe(59);
});
});
describe('epochDaysFromTime', () => {
it('should return the epoch day for day 1', () => {
const days: Date[] = CalendarTools.epochDaysFromTime(1, '07:00');
expect(days).toHaveLength(1);
expect(days[0].getUTCFullYear()).toBe(1969);
expect(days[0].getUTCMonth()).toBe(11);
expect(days[0].getUTCDate()).toBe(29);
});
it('should return the epoch day for day 2', () => {
const days: Date[] = CalendarTools.epochDaysFromTime(2, '07:00');
expect(days).toHaveLength(1);
expect(days[0].getUTCFullYear()).toBe(1969);
expect(days[0].getUTCMonth()).toBe(11);
expect(days[0].getUTCDate()).toBe(30);
});
it('should return the epoch day for day 3', () => {
const days: Date[] = CalendarTools.epochDaysFromTime(3, '07:00');
expect(days).toHaveLength(1);
expect(days[0].getUTCFullYear()).toBe(1969);
expect(days[0].getUTCMonth()).toBe(11);
expect(days[0].getUTCDate()).toBe(31);
});
it('should return the epoch day for day 4', () => {
const days: Date[] = CalendarTools.epochDaysFromTime(4, '07:00');
expect(days).toHaveLength(1);
expect(days[0].getUTCFullYear()).toBe(1970);
expect(days[0].getUTCMonth()).toBe(0);
expect(days[0].getUTCDate()).toBe(1);
});
it('should return the epoch day for day 5', () => {
const days: Date[] = CalendarTools.epochDaysFromTime(5, '07:00');
expect(days).toHaveLength(1);
expect(days[0].getUTCFullYear()).toBe(1970);
expect(days[0].getUTCMonth()).toBe(0);
expect(days[0].getUTCDate()).toBe(2);
});
it('should return the epoch days for day 0', () => {
const days: Date[] = CalendarTools.epochDaysFromTime(0, '07:00');
expect(days).toHaveLength(2);
expect(days[0].getUTCFullYear()).toBe(1969);
expect(days[0].getUTCMonth()).toBe(11);
expect(days[0].getUTCDate()).toBe(28);
expect(days[1].getUTCFullYear()).toBe(1970);
expect(days[1].getUTCMonth()).toBe(0);
expect(days[1].getUTCDate()).toBe(4);
});
it('should return the epoch days for day 6', () => {
const days: Date[] = CalendarTools.epochDaysFromTime(6, '07:00');
expect(days).toHaveLength(2);
expect(days[0].getUTCFullYear()).toBe(1969);
expect(days[0].getUTCMonth()).toBe(11);
expect(days[0].getUTCDate()).toBe(27);
expect(days[1].getUTCFullYear()).toBe(1970);
expect(days[1].getUTCMonth()).toBe(0);
expect(days[1].getUTCDate()).toBe(3);
});
it('should throw an exception if a given week day is invalid', () => {
expect(() => {
CalendarTools.epochDaysFromTime(8, '07:00');
}).toThrow(CalendarToolsException); }).toThrow(CalendarToolsException);
}); });
}); });

View File

@ -2,13 +2,16 @@ import { Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { Target } from '@modules/ad/core/domain/candidate.types'; import { Target } from '@modules/ad/core/domain/candidate.types';
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
describe('Candidate entity', () => { describe('Candidate entity', () => {
it('should create a new candidate entity', () => { it('should create a new candidate entity', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({ const candidateEntity: CandidateEntity = CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
dateInterval: {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: [ driverWaypoints: [
{ {
lat: 48.678454, lat: 48.678454,
@ -52,10 +55,15 @@ describe('Candidate entity', () => {
}); });
expect(candidateEntity.id.length).toBe(36); expect(candidateEntity.id.length).toBe(36);
}); });
it('should set a candidate entity carpool path', () => { it('should set a candidate entity carpool path', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({ const candidateEntity: CandidateEntity = CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
role: Role.PASSENGER, role: Role.PASSENGER,
dateInterval: {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: [ driverWaypoints: [
{ {
lat: 48.689445, lat: 48.689445,
@ -98,10 +106,8 @@ describe('Candidate entity', () => {
}, },
}).setCarpoolPath([ }).setCarpoolPath([
{ {
point: new Point({
lat: 48.689445, lat: 48.689445,
lon: 6.17651, lon: 6.17651,
}),
actors: [ actors: [
new Actor({ new Actor({
role: Role.DRIVER, role: Role.DRIVER,
@ -114,10 +120,8 @@ describe('Candidate entity', () => {
], ],
}, },
{ {
point: new Point({
lat: 48.8566, lat: 48.8566,
lon: 2.3522, lon: 2.3522,
}),
actors: [ actors: [
new Actor({ new Actor({
role: Role.DRIVER, role: Role.DRIVER,
@ -130,12 +134,17 @@ describe('Candidate entity', () => {
], ],
}, },
]); ]);
expect(candidateEntity.getProps().carpoolSteps).toHaveLength(2); expect(candidateEntity.getProps().carpoolPath).toHaveLength(2);
}); });
it('should create a new candidate entity with spacetime metrics', () => { it('should create a new candidate entity with spacetime metrics', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({ const candidateEntity: CandidateEntity = CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
dateInterval: {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: [ driverWaypoints: [
{ {
lat: 48.678454, lat: 48.678454,
@ -180,10 +189,16 @@ describe('Candidate entity', () => {
expect(candidateEntity.getProps().distance).toBe(352688); expect(candidateEntity.getProps().distance).toBe(352688);
expect(candidateEntity.getProps().duration).toBe(14587); expect(candidateEntity.getProps().duration).toBe(14587);
}); });
describe('detour validation', () => {
it('should not validate a candidate entity with exceeding distance detour', () => { it('should not validate a candidate entity with exceeding distance detour', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({ const candidateEntity: CandidateEntity = CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
dateInterval: {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: [ driverWaypoints: [
{ {
lat: 48.678454, lat: 48.678454,
@ -231,6 +246,10 @@ describe('Candidate entity', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({ const candidateEntity: CandidateEntity = CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
dateInterval: {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: [ driverWaypoints: [
{ {
lat: 48.678454, lat: 48.678454,
@ -275,3 +294,8 @@ describe('Candidate entity', () => {
expect(candidateEntity.isDetourValid()).toBeFalsy(); expect(candidateEntity.isDetourValid()).toBeFalsy();
}); });
}); });
describe('Journeys', () => {
it('should create journeys', () => {});
});
});

View File

@ -1,7 +1,7 @@
import { CarpoolPathCreator } from '@modules/ad/core/domain/carpool-path-creator.service'; import { CarpoolPathCreator } from '@modules/ad/core/domain/carpool-path-creator.service';
import { CarpoolPathCreatorException } from '@modules/ad/core/domain/match.errors'; import { CarpoolPathCreatorException } from '@modules/ad/core/domain/match.errors';
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
import { CarpoolStep } from '@modules/ad/core/domain/value-objects/carpool-step.value-object'; import { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object';
const waypoint1: Point = new Point({ const waypoint1: Point = new Point({
lat: 0, lat: 0,
@ -34,71 +34,71 @@ describe('Carpool Path Creator Service', () => {
[waypoint1, waypoint6], [waypoint1, waypoint6],
[waypoint2, waypoint5], [waypoint2, waypoint5],
); );
const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath();
expect(carpoolSteps).toHaveLength(4); expect(carpoolPath).toHaveLength(4);
expect(carpoolSteps[0].actors.length).toBe(1); expect(carpoolPath[0].actors.length).toBe(1);
}); });
it('should create a simple carpool path with same destination for driver and passenger', () => { it('should create a simple carpool path with same destination for driver and passenger', () => {
const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator(
[waypoint1, waypoint6], [waypoint1, waypoint6],
[waypoint2, waypoint6], [waypoint2, waypoint6],
); );
const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath();
expect(carpoolSteps).toHaveLength(3); expect(carpoolPath).toHaveLength(3);
expect(carpoolSteps[0].actors.length).toBe(1); expect(carpoolPath[0].actors.length).toBe(1);
expect(carpoolSteps[1].actors.length).toBe(2); expect(carpoolPath[1].actors.length).toBe(2);
expect(carpoolSteps[2].actors.length).toBe(2); expect(carpoolPath[2].actors.length).toBe(2);
}); });
it('should create a simple carpool path with same waypoints for driver and passenger', () => { it('should create a simple carpool path with same waypoints for driver and passenger', () => {
const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator(
[waypoint1, waypoint6], [waypoint1, waypoint6],
[waypoint1, waypoint6], [waypoint1, waypoint6],
); );
const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath();
expect(carpoolSteps).toHaveLength(2); expect(carpoolPath).toHaveLength(2);
expect(carpoolSteps[0].actors.length).toBe(2); expect(carpoolPath[0].actors.length).toBe(2);
expect(carpoolSteps[1].actors.length).toBe(2); expect(carpoolPath[1].actors.length).toBe(2);
}); });
it('should create a complex carpool path with 3 driver waypoints', () => { it('should create a complex carpool path with 3 driver waypoints', () => {
const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator(
[waypoint1, waypoint3, waypoint6], [waypoint1, waypoint3, waypoint6],
[waypoint2, waypoint5], [waypoint2, waypoint5],
); );
const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath();
expect(carpoolSteps).toHaveLength(5); expect(carpoolPath).toHaveLength(5);
expect(carpoolSteps[0].actors.length).toBe(1); expect(carpoolPath[0].actors.length).toBe(1);
expect(carpoolSteps[1].actors.length).toBe(2); expect(carpoolPath[1].actors.length).toBe(2);
expect(carpoolSteps[2].actors.length).toBe(1); expect(carpoolPath[2].actors.length).toBe(1);
expect(carpoolSteps[3].actors.length).toBe(2); expect(carpoolPath[3].actors.length).toBe(2);
expect(carpoolSteps[4].actors.length).toBe(1); expect(carpoolPath[4].actors.length).toBe(1);
}); });
it('should create a complex carpool path with 4 driver waypoints', () => { it('should create a complex carpool path with 4 driver waypoints', () => {
const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator(
[waypoint1, waypoint3, waypoint4, waypoint6], [waypoint1, waypoint3, waypoint4, waypoint6],
[waypoint2, waypoint5], [waypoint2, waypoint5],
); );
const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath();
expect(carpoolSteps).toHaveLength(6); expect(carpoolPath).toHaveLength(6);
expect(carpoolSteps[0].actors.length).toBe(1); expect(carpoolPath[0].actors.length).toBe(1);
expect(carpoolSteps[1].actors.length).toBe(2); expect(carpoolPath[1].actors.length).toBe(2);
expect(carpoolSteps[2].actors.length).toBe(1); expect(carpoolPath[2].actors.length).toBe(1);
expect(carpoolSteps[3].actors.length).toBe(1); expect(carpoolPath[3].actors.length).toBe(1);
expect(carpoolSteps[4].actors.length).toBe(2); expect(carpoolPath[4].actors.length).toBe(2);
expect(carpoolSteps[5].actors.length).toBe(1); expect(carpoolPath[5].actors.length).toBe(1);
}); });
it('should create a alternate complex carpool path with 4 driver waypoints', () => { it('should create a alternate complex carpool path with 4 driver waypoints', () => {
const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator(
[waypoint1, waypoint2, waypoint5, waypoint6], [waypoint1, waypoint2, waypoint5, waypoint6],
[waypoint3, waypoint4], [waypoint3, waypoint4],
); );
const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath();
expect(carpoolSteps).toHaveLength(6); expect(carpoolPath).toHaveLength(6);
expect(carpoolSteps[0].actors.length).toBe(1); expect(carpoolPath[0].actors.length).toBe(1);
expect(carpoolSteps[1].actors.length).toBe(1); expect(carpoolPath[1].actors.length).toBe(1);
expect(carpoolSteps[2].actors.length).toBe(2); expect(carpoolPath[2].actors.length).toBe(2);
expect(carpoolSteps[3].actors.length).toBe(2); expect(carpoolPath[3].actors.length).toBe(2);
expect(carpoolSteps[4].actors.length).toBe(1); expect(carpoolPath[4].actors.length).toBe(1);
expect(carpoolSteps[5].actors.length).toBe(1); expect(carpoolPath[5].actors.length).toBe(1);
}); });
it('should throw an exception if less than 2 driver waypoints are given', () => { it('should throw an exception if less than 2 driver waypoints are given', () => {
expect(() => { expect(() => {

View File

@ -2,16 +2,13 @@ import { ArgumentOutOfRangeException } from '@mobicoop/ddd-library';
import { Role } from '@modules/ad/core/domain/ad.types'; import { Role } from '@modules/ad/core/domain/ad.types';
import { Target } from '@modules/ad/core/domain/candidate.types'; import { Target } from '@modules/ad/core/domain/candidate.types';
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; import { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object';
import { CarpoolStep } from '@modules/ad/core/domain/value-objects/carpool-step.value-object';
describe('CarpoolStep value object', () => { describe('Carpool Path Item value object', () => {
it('should create a carpoolStep value object', () => { it('should create a path item value object', () => {
const carpoolStepVO = new CarpoolStep({ const carpoolPathItemVO = new CarpoolPathItem({
point: new Point({
lat: 48.689445, lat: 48.689445,
lon: 6.17651, lon: 6.17651,
}),
actors: [ actors: [
new Actor({ new Actor({
role: Role.DRIVER, role: Role.DRIVER,
@ -23,28 +20,24 @@ describe('CarpoolStep value object', () => {
}), }),
], ],
}); });
expect(carpoolStepVO.point.lon).toBe(6.17651); expect(carpoolPathItemVO.lon).toBe(6.17651);
expect(carpoolStepVO.point.lat).toBe(48.689445); expect(carpoolPathItemVO.lat).toBe(48.689445);
expect(carpoolStepVO.actors).toHaveLength(2); expect(carpoolPathItemVO.actors).toHaveLength(2);
}); });
it('should throw an exception if actors is empty', () => { it('should throw an exception if actors is empty', () => {
expect(() => { expect(() => {
new CarpoolStep({ new CarpoolPathItem({
point: new Point({
lat: 48.689445, lat: 48.689445,
lon: 6.17651, lon: 6.17651,
}),
actors: [], actors: [],
}); });
}).toThrow(ArgumentOutOfRangeException); }).toThrow(ArgumentOutOfRangeException);
}); });
it('should throw an exception if actors contains more than one driver', () => { it('should throw an exception if actors contains more than one driver', () => {
expect(() => { expect(() => {
new CarpoolStep({ new CarpoolPathItem({
point: new Point({
lat: 48.689445, lat: 48.689445,
lon: 6.17651, lon: 6.17651,
}),
actors: [ actors: [
new Actor({ new Actor({
role: Role.DRIVER, role: Role.DRIVER,

View File

@ -28,7 +28,9 @@ describe('Journey item value object', () => {
expect(journeyItemVO.distance).toBe(48754); expect(journeyItemVO.distance).toBe(48754);
expect(journeyItemVO.lon).toBe(6.17651); expect(journeyItemVO.lon).toBe(6.17651);
expect(journeyItemVO.lat).toBe(48.689445); expect(journeyItemVO.lat).toBe(48.689445);
expect(journeyItemVO.actorTimes[0].firstMaxDatetime.getMinutes()).toBe(15); expect(journeyItemVO.actorTimes[0].firstMaxDatetime.getUTCMinutes()).toBe(
15,
);
}); });
it('should throw an error if actorTimes is too short', () => { it('should throw an error if actorTimes is too short', () => {
expect( expect(

View File

@ -6,7 +6,6 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { Target } from '@modules/ad/core/domain/candidate.types'; import { Target } from '@modules/ad/core/domain/candidate.types';
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
const originWaypoint: Waypoint = { const originWaypoint: Waypoint = {
position: 0, position: 0,
@ -66,6 +65,10 @@ const matchQuery = new MatchQuery(
const candidate: CandidateEntity = CandidateEntity.create({ const candidate: CandidateEntity = CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
dateInterval: {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: [ driverWaypoints: [
{ {
lat: 48.678454, lat: 48.678454,
@ -108,10 +111,8 @@ const candidate: CandidateEntity = CandidateEntity.create({
}, },
}).setCarpoolPath([ }).setCarpoolPath([
{ {
point: new Point({
lat: 48.689445, lat: 48.689445,
lon: 6.17651, lon: 6.17651,
}),
actors: [ actors: [
new Actor({ new Actor({
role: Role.DRIVER, role: Role.DRIVER,
@ -124,10 +125,8 @@ const candidate: CandidateEntity = CandidateEntity.create({
], ],
}, },
{ {
point: new Point({
lat: 48.8566, lat: 48.8566,
lon: 2.3522, lon: 2.3522,
}),
actors: [ actors: [
new Actor({ new Actor({
role: Role.DRIVER, role: Role.DRIVER,

View File

@ -1,7 +1,4 @@
import { import { ArgumentInvalidException } from '@mobicoop/ddd-library';
ArgumentInvalidException,
ArgumentOutOfRangeException,
} from '@mobicoop/ddd-library';
import { Role } from '@modules/ad/core/domain/ad.types'; import { Role } from '@modules/ad/core/domain/ad.types';
import { Target } from '@modules/ad/core/domain/candidate.types'; import { Target } from '@modules/ad/core/domain/candidate.types';
import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object';
@ -13,7 +10,6 @@ describe('Journey value object', () => {
const journeyVO = new Journey({ const journeyVO = new Journey({
firstDate: new Date('2023-09-01'), firstDate: new Date('2023-09-01'),
lastDate: new Date('2024-08-30'), lastDate: new Date('2024-08-30'),
day: 5,
journeyItems: [ journeyItems: [
new JourneyItem({ new JourneyItem({
lat: 48.689445, lat: 48.689445,
@ -109,114 +105,9 @@ describe('Journey value object', () => {
}), }),
], ],
}); });
expect(journeyVO.day).toBe(5);
expect(journeyVO.journeyItems).toHaveLength(4); expect(journeyVO.journeyItems).toHaveLength(4);
expect(journeyVO.firstDate.getDate()).toBe(1); expect(journeyVO.firstDate.getUTCDate()).toBe(1);
expect(journeyVO.lastDate.getMonth()).toBe(7); expect(journeyVO.lastDate.getUTCMonth()).toBe(7);
});
it('should throw an error if day is wrong', () => {
expect(
() =>
new Journey({
firstDate: new Date('2023-09-01'),
lastDate: new Date('2024-08-30'),
day: 7,
journeyItems: [
new JourneyItem({
lat: 48.689445,
lon: 6.17651,
duration: 0,
distance: 0,
actorTimes: [
new ActorTime({
role: Role.DRIVER,
target: Target.START,
firstDatetime: new Date('2023-09-01 07:00'),
firstMinDatetime: new Date('2023-09-01 06:45'),
firstMaxDatetime: new Date('2023-09-01 07:15'),
lastDatetime: new Date('2024-08-30 07:00'),
lastMinDatetime: new Date('2024-08-30 06:45'),
lastMaxDatetime: new Date('2024-08-30 07:15'),
}),
],
}),
new JourneyItem({
lat: 48.369445,
lon: 6.67487,
duration: 2100,
distance: 56878,
actorTimes: [
new ActorTime({
role: Role.DRIVER,
target: Target.NEUTRAL,
firstDatetime: new Date('2023-09-01 07:35'),
firstMinDatetime: new Date('2023-09-01 07:20'),
firstMaxDatetime: new Date('2023-09-01 07:50'),
lastDatetime: new Date('2024-08-30 07:35'),
lastMinDatetime: new Date('2024-08-30 07:20'),
lastMaxDatetime: new Date('2024-08-30 07:50'),
}),
new ActorTime({
role: Role.PASSENGER,
target: Target.START,
firstDatetime: new Date('2023-09-01 07:32'),
firstMinDatetime: new Date('2023-09-01 07:17'),
firstMaxDatetime: new Date('2023-09-01 07:47'),
lastDatetime: new Date('2024-08-30 07:32'),
lastMinDatetime: new Date('2024-08-30 07:17'),
lastMaxDatetime: new Date('2024-08-30 07:47'),
}),
],
}),
new JourneyItem({
lat: 47.98487,
lon: 6.9427,
duration: 3840,
distance: 76491,
actorTimes: [
new ActorTime({
role: Role.DRIVER,
target: Target.NEUTRAL,
firstDatetime: new Date('2023-09-01 08:04'),
firstMinDatetime: new Date('2023-09-01 07:51'),
firstMaxDatetime: new Date('2023-09-01 08:19'),
lastDatetime: new Date('2024-08-30 08:04'),
lastMinDatetime: new Date('2024-08-30 07:51'),
lastMaxDatetime: new Date('2024-08-30 08:19'),
}),
new ActorTime({
role: Role.PASSENGER,
target: Target.FINISH,
firstDatetime: new Date('2023-09-01 08:01'),
firstMinDatetime: new Date('2023-09-01 07:46'),
firstMaxDatetime: new Date('2023-09-01 08:16'),
lastDatetime: new Date('2024-08-30 08:01'),
lastMinDatetime: new Date('2024-08-30 07:46'),
lastMaxDatetime: new Date('2024-08-30 08:16'),
}),
],
}),
new JourneyItem({
lat: 47.365987,
lon: 7.02154,
duration: 4980,
distance: 96475,
actorTimes: [
new ActorTime({
role: Role.DRIVER,
target: Target.FINISH,
firstDatetime: new Date('2023-09-01 08:23'),
firstMinDatetime: new Date('2023-09-01 08:08'),
firstMaxDatetime: new Date('2023-09-01 08:38'),
lastDatetime: new Date('2024-08-30 08:23'),
lastMinDatetime: new Date('2024-08-30 08:08'),
lastMaxDatetime: new Date('2024-08-30 08:38'),
}),
],
}),
],
}),
).toThrow(ArgumentOutOfRangeException);
}); });
it('should throw an error if dates are inconsistent', () => { it('should throw an error if dates are inconsistent', () => {
expect( expect(
@ -224,7 +115,6 @@ describe('Journey value object', () => {
new Journey({ new Journey({
firstDate: new Date('2023-09-01'), firstDate: new Date('2023-09-01'),
lastDate: new Date('2024-08-31'), lastDate: new Date('2024-08-31'),
day: 5,
journeyItems: [ journeyItems: [
new JourneyItem({ new JourneyItem({
lat: 48.689445, lat: 48.689445,
@ -326,7 +216,6 @@ describe('Journey value object', () => {
new Journey({ new Journey({
firstDate: new Date('2024-08-30'), firstDate: new Date('2024-08-30'),
lastDate: new Date('2023-09-01'), lastDate: new Date('2023-09-01'),
day: 5,
journeyItems: [ journeyItems: [
new JourneyItem({ new JourneyItem({
lat: 48.689445, lat: 48.689445,
@ -430,7 +319,6 @@ describe('Journey value object', () => {
new Journey({ new Journey({
firstDate: new Date('2023-09-01'), firstDate: new Date('2023-09-01'),
lastDate: new Date('2024-08-30'), lastDate: new Date('2024-08-30'),
day: 5,
journeyItems: [ journeyItems: [
new JourneyItem({ new JourneyItem({
lat: 48.689445, lat: 48.689445,

View File

@ -50,6 +50,10 @@ const candidates: CandidateEntity[] = [
CandidateEntity.create({ CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
dateInterval: {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: [ driverWaypoints: [
{ {
lat: 48.678454, lat: 48.678454,
@ -94,6 +98,10 @@ const candidates: CandidateEntity[] = [
CandidateEntity.create({ CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
role: Role.PASSENGER, role: Role.PASSENGER,
dateInterval: {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: [ driverWaypoints: [
{ {
lat: 48.689445, lat: 48.689445,

View File

@ -49,6 +49,10 @@ const matchQuery = new MatchQuery(
const candidate: CandidateEntity = CandidateEntity.create({ const candidate: CandidateEntity = CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
dateInterval: {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: [ driverWaypoints: [
{ {
lat: 48.678454, lat: 48.678454,

View File

@ -9,7 +9,6 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { Target } from '@modules/ad/core/domain/candidate.types'; import { Target } from '@modules/ad/core/domain/candidate.types';
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
const originWaypoint: Waypoint = { const originWaypoint: Waypoint = {
position: 0, position: 0,
@ -70,6 +69,10 @@ const matchQuery = new MatchQuery(
const candidate: CandidateEntity = CandidateEntity.create({ const candidate: CandidateEntity = CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
dateInterval: {
lowerDate: '2023-08-28',
higherDate: '2023-08-28',
},
driverWaypoints: [ driverWaypoints: [
{ {
lat: 48.678454, lat: 48.678454,
@ -112,10 +115,8 @@ const candidate: CandidateEntity = CandidateEntity.create({
}, },
}).setCarpoolPath([ }).setCarpoolPath([
{ {
point: new Point({
lat: 48.689445, lat: 48.689445,
lon: 6.17651, lon: 6.17651,
}),
actors: [ actors: [
new Actor({ new Actor({
role: Role.DRIVER, role: Role.DRIVER,
@ -128,10 +129,8 @@ const candidate: CandidateEntity = CandidateEntity.create({
], ],
}, },
{ {
point: new Point({
lat: 48.8566, lat: 48.8566,
lon: 2.3522, lon: 2.3522,
}),
actors: [ actors: [
new Actor({ new Actor({
role: Role.DRIVER, role: Role.DRIVER,

View File

@ -164,12 +164,14 @@ export class GraphhopperGeorouter implements GeorouterPort {
points: [[number, number]], points: [[number, number]],
snappedWaypoints: [[number, number]], snappedWaypoints: [[number, number]],
): number[] => { ): number[] => {
const indices = snappedWaypoints.map((waypoint) => const indices: number[] = snappedWaypoints.map(
(waypoint: [number, number]) =>
points.findIndex( points.findIndex(
(point) => point[0] == waypoint[0] && point[1] == waypoint[1], (point) => point[0] == waypoint[0] && point[1] == waypoint[1],
), ),
); );
if (indices.find((index) => index == -1) === undefined) return indices; if (indices.find((index: number) => index == -1) === undefined)
return indices;
const missedWaypoints = indices const missedWaypoints = indices
.map( .map(
(value, index) => (value, index) =>