From 467d8a84f8aee2fc177ab71dcffd5db4f8a4d0c5 Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 22 Sep 2023 16:37:52 +0200 Subject: [PATCH] wip - create journeys - no tests yet --- .../queries/match/algorithm.abstract.ts | 2 +- .../match/completer/journey.completer.ts | 4 +- .../match/completer/route.completer.ts | 10 +- .../application/queries/match/match.query.ts | 2 +- .../selector/passenger-oriented.selector.ts | 46 ++-- .../ad/core/domain/calendar-tools.service.ts | 105 +++++--- .../ad/core/domain/candidate.entity.ts | 201 ++++++++++++--- src/modules/ad/core/domain/candidate.types.ts | 23 +- .../domain/carpool-path-creator.service.ts | 234 ++++++++++-------- .../value-objects/actor-time.value-object.ts | 2 +- ...t.ts => carpool-path-item.value-object.ts} | 22 +- .../value-objects/journey.value-object.ts | 15 +- .../input-datetime-transformer.ts | 2 +- .../ad/infrastructure/time-converter.ts | 4 +- .../unit/core/actor-time.value-object.spec.ts | 96 +++---- .../unit/core/algorithm.abstract.spec.ts | 4 + .../unit/core/calendar-tools.service.spec.ts | 201 +++++++++++---- .../tests/unit/core/candidate.entity.spec.ts | 220 ++++++++-------- .../core/carpool-path-creator.service.spec.ts | 72 +++--- ...=> carpool-path-item.value-object.spec.ts} | 37 ++- .../core/journey-item.value-object.spec.ts | 4 +- .../tests/unit/core/journey.completer.spec.ts | 17 +- .../unit/core/journey.value-object.spec.ts | 118 +-------- ...er-oriented-carpool-path-completer.spec.ts | 8 + .../passenger-oriented-geo-filter.spec.ts | 4 + .../tests/unit/core/route.completer.spec.ts | 17 +- .../infrastructure/graphhopper-georouter.ts | 12 +- 27 files changed, 860 insertions(+), 622 deletions(-) rename src/modules/ad/core/domain/value-objects/{carpool-step.value-object.ts => carpool-path-item.value-object.ts} (61%) rename src/modules/ad/tests/unit/core/{carpool-step.value-object.spec.ts => carpool-path-item.value-object.spec.ts} (60%) diff --git a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts index faeb9bc..6915366 100644 --- a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts +++ b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts @@ -20,7 +20,7 @@ export abstract class Algorithm { for (const processor of this.processors) { this.candidates = await processor.execute(this.candidates); } - // console.log(JSON.stringify(this.candidates, null, 2)); + console.log(JSON.stringify(this.candidates, null, 2)); return this.candidates.map((candidate: CandidateEntity) => MatchEntity.create({ adId: candidate.id }), ); diff --git a/src/modules/ad/core/application/queries/match/completer/journey.completer.ts b/src/modules/ad/core/application/queries/match/completer/journey.completer.ts index b50ebf5..71469bc 100644 --- a/src/modules/ad/core/application/queries/match/completer/journey.completer.ts +++ b/src/modules/ad/core/application/queries/match/completer/journey.completer.ts @@ -5,7 +5,5 @@ export class JourneyCompleter extends Completer { complete = async ( candidates: CandidateEntity[], ): Promise => - candidates.map((candidate: CandidateEntity) => - candidate.createJourneys(this.query.fromDate, this.query.toDate), - ); + candidates.map((candidate: CandidateEntity) => candidate.createJourneys()); } diff --git a/src/modules/ad/core/application/queries/match/completer/route.completer.ts b/src/modules/ad/core/application/queries/match/completer/route.completer.ts index 96dc9f1..5a07e10 100644 --- a/src/modules/ad/core/application/queries/match/completer/route.completer.ts +++ b/src/modules/ad/core/application/queries/match/completer/route.completer.ts @@ -1,8 +1,8 @@ import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Completer } from './completer.abstract'; 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 { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object'; export class RouteCompleter extends Completer { protected readonly type: RouteCompleterType; @@ -19,8 +19,8 @@ export class RouteCompleter extends Completer { switch (this.type) { case RouteCompleterType.BASIC: const basicCandidateRoute = await this.query.routeProvider.getBasic( - (candidate.getProps().carpoolSteps as CarpoolStep[]).map( - (carpoolStep: CarpoolStep) => carpoolStep.point, + (candidate.getProps().carpoolPath as CarpoolPathItem[]).map( + (carpoolPathItem: CarpoolPathItem) => carpoolPathItem, ), ); candidate.setMetrics( @@ -31,8 +31,8 @@ export class RouteCompleter extends Completer { case RouteCompleterType.DETAILED: const detailedCandidateRoute = await this.query.routeProvider.getDetailed( - (candidate.getProps().carpoolSteps as CarpoolStep[]).map( - (carpoolStep: CarpoolStep) => carpoolStep.point, + (candidate.getProps().carpoolPath as CarpoolPathItem[]).map( + (carpoolPathItem: CarpoolPathItem) => carpoolPathItem, ), ); candidate.setSteps(detailedCandidateRoute.steps as Step[]); diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index 337ae7c..742a663 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -153,7 +153,7 @@ export class MatchQuery extends QueryBase { ); this.schedule = this.schedule.map((scheduleItem: ScheduleItem) => ({ day: datetimeTransformer.day( - scheduleItem.day ?? new Date(this.fromDate).getDay(), + scheduleItem.day ?? new Date(this.fromDate).getUTCDay(), { date: this.fromDate, time: scheduleItem.time, diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index 7768fba..ebb1cbd 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -36,6 +36,16 @@ export class PassengerOrientedSelector extends Selector { CandidateEntity.create({ id: adEntity.id, 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: adsRole.role == Role.PASSENGER ? adEntity.getProps().waypoints @@ -173,7 +183,7 @@ export class PassengerOrientedSelector extends Selector { scheduleDates.map((date: Date) => { this.query.schedule .filter( - (scheduleItem: ScheduleItem) => date.getDay() == scheduleItem.day, + (scheduleItem: ScheduleItem) => date.getUTCDay() == scheduleItem.day, ) .map((scheduleItem: ScheduleItem) => { 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 return `make_timestamp(\ - ${maxDepartureDatetime.getFullYear()},\ - ${maxDepartureDatetime.getMonth() + 1},\ - ${maxDepartureDatetime.getDate()},\ + ${maxDepartureDatetime.getUTCFullYear()},\ + ${maxDepartureDatetime.getUTCMonth() + 1},\ + ${maxDepartureDatetime.getUTCDate()},\ CAST(EXTRACT(hour from time) as integer),\ CAST(EXTRACT(minute from time) as integer),0) - interval '1 second' * margin <=\ make_timestamp(\ - ${maxDepartureDatetime.getFullYear()},\ - ${maxDepartureDatetime.getMonth() + 1},\ - ${maxDepartureDatetime.getDate()},${maxDepartureDatetime.getHours()},${maxDepartureDatetime.getMinutes()},0)`; + ${maxDepartureDatetime.getUTCFullYear()},\ + ${maxDepartureDatetime.getUTCMonth() + 1},\ + ${maxDepartureDatetime.getUTCDate()},${maxDepartureDatetime.getUTCHours()},${maxDepartureDatetime.getUTCMinutes()},0)`; }; 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 return `make_timestamp(\ - ${minDepartureDatetime.getFullYear()}, - ${minDepartureDatetime.getMonth() + 1}, - ${minDepartureDatetime.getDate()},\ + ${minDepartureDatetime.getUTCFullYear()}, + ${minDepartureDatetime.getUTCMonth() + 1}, + ${minDepartureDatetime.getUTCDate()},\ CAST(EXTRACT(hour from time) as integer),\ CAST(EXTRACT(minute from time) as integer),0) + interval '1 second' * margin >=\ make_timestamp(\ - ${minDepartureDatetime.getFullYear()}, - ${minDepartureDatetime.getMonth() + 1}, - ${minDepartureDatetime.getDate()},${minDepartureDatetime.getHours()},${minDepartureDatetime.getMinutes()},0)`; + ${minDepartureDatetime.getUTCFullYear()}, + ${minDepartureDatetime.getUTCMonth() + 1}, + ${minDepartureDatetime.getUTCDate()},${minDepartureDatetime.getUTCHours()},${minDepartureDatetime.getUTCMinutes()},0)`; }; private _whereAzimuth = (): string => { @@ -311,7 +321,7 @@ export class PassengerOrientedSelector extends Selector { for ( let date = fromDate; date <= toDate; - date.setDate(date.getDate() + 1) + date.setUTCDate(date.getUTCDate() + 1) ) { dates.push(new Date(date)); count++; @@ -321,7 +331,7 @@ export class PassengerOrientedSelector extends Selector { }; private _addMargin = (date: Date, marginInSeconds: number): Date => { - date.setTime(date.getTime() + marginInSeconds * 1000); + date.setUTCSeconds(marginInSeconds); return date; }; @@ -334,6 +344,12 @@ export class PassengerOrientedSelector extends Selector { maxAzimuth: 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 = { diff --git a/src/modules/ad/core/domain/calendar-tools.service.ts b/src/modules/ad/core/domain/calendar-tools.service.ts index b0d2b4f..184adc6 100644 --- a/src/modules/ad/core/domain/calendar-tools.service.ts +++ b/src/modules/ad/core/domain/calendar-tools.service.ts @@ -1,31 +1,29 @@ import { ExceptionBase } from '@mobicoop/ddd-library'; +import { DateInterval } from './candidate.types'; export class CalendarTools { /** * Returns the first date corresponding to a week day (0 based monday) * within a date range */ - static firstDate = ( - weekDay: number, - lowerDate: string, - higherDate: string, - ): Date => { + static firstDate = (weekDay: number, dateInterval: DateInterval): Date => { if (weekDay < 0 || weekDay > 6) throw new CalendarToolsException( new Error('weekDay must be between 0 and 6'), ); - const lowerDateAsDate: Date = new Date(lowerDate); - const higherDateAsDate: Date = new Date(higherDate); - if (lowerDateAsDate.getDay() == weekDay) return lowerDateAsDate; + const lowerDateAsDate: Date = new Date(dateInterval.lowerDate); + const higherDateAsDate: Date = new Date(dateInterval.higherDate); + if (lowerDateAsDate.getUTCDay() == weekDay) return lowerDateAsDate; const nextDate: Date = new Date(lowerDateAsDate); - nextDate.setDate( - lowerDateAsDate.getDate() + (7 - (lowerDateAsDate.getDay() - weekDay)), + nextDate.setUTCDate( + lowerDateAsDate.getUTCDate() + + (7 - (lowerDateAsDate.getUTCDay() - weekDay)), ); - if (lowerDateAsDate.getDay() < weekDay) { - nextDate.setMonth(lowerDateAsDate.getMonth()); - nextDate.setFullYear(lowerDateAsDate.getFullYear()); - nextDate.setDate( - lowerDateAsDate.getDate() + (weekDay - lowerDateAsDate.getDay()), + if (lowerDateAsDate.getUTCDay() < weekDay) { + nextDate.setUTCMonth(lowerDateAsDate.getUTCMonth()); + nextDate.setUTCFullYear(lowerDateAsDate.getUTCFullYear()); + nextDate.setUTCDate( + lowerDateAsDate.getUTCDate() + (weekDay - lowerDateAsDate.getUTCDay()), ); } if (nextDate <= higherDateAsDate) return nextDate; @@ -38,28 +36,24 @@ export class CalendarTools { * Returns the last date corresponding to a week day (0 based monday) * within a date range */ - static lastDate = ( - weekDay: number, - lowerDate: string, - higherDate: string, - ): Date => { + static lastDate = (weekDay: number, dateInterval: DateInterval): Date => { if (weekDay < 0 || weekDay > 6) throw new CalendarToolsException( new Error('weekDay must be between 0 and 6'), ); - const lowerDateAsDate: Date = new Date(lowerDate); - const higherDateAsDate: Date = new Date(higherDate); - if (higherDateAsDate.getDay() == weekDay) return higherDateAsDate; + const lowerDateAsDate: Date = new Date(dateInterval.lowerDate); + const higherDateAsDate: Date = new Date(dateInterval.higherDate); + if (higherDateAsDate.getUTCDay() == weekDay) return higherDateAsDate; const previousDate: Date = new Date(higherDateAsDate); - previousDate.setDate( - higherDateAsDate.getDate() - (higherDateAsDate.getDay() - weekDay), + previousDate.setUTCDate( + higherDateAsDate.getUTCDate() - (higherDateAsDate.getUTCDay() - weekDay), ); - if (higherDateAsDate.getDay() < weekDay) { - previousDate.setMonth(higherDateAsDate.getMonth()); - previousDate.setFullYear(higherDateAsDate.getFullYear()); - previousDate.setDate( - higherDateAsDate.getDate() - - (7 + (higherDateAsDate.getDay() - weekDay)), + if (higherDateAsDate.getUTCDay() < weekDay) { + previousDate.setUTCMonth(higherDateAsDate.getUTCMonth()); + previousDate.setUTCFullYear(higherDateAsDate.getUTCFullYear()); + previousDate.setUTCDate( + higherDateAsDate.getUTCDate() - + (7 + (higherDateAsDate.getUTCDay() - weekDay)), ); } if (previousDate >= lowerDateAsDate) return previousDate; @@ -67,6 +61,55 @@ export class CalendarTools { 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 { diff --git a/src/modules/ad/core/domain/candidate.entity.ts b/src/modules/ad/core/domain/candidate.entity.ts index 4bd0143..073dcea 100644 --- a/src/modules/ad/core/domain/candidate.entity.ts +++ b/src/modules/ad/core/domain/candidate.entity.ts @@ -1,11 +1,21 @@ import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; -import { CandidateProps, CreateCandidateProps } from './candidate.types'; -import { CarpoolStepProps } from './value-objects/carpool-step.value-object'; -import { StepProps } from './value-objects/step.value-object'; +import { + CandidateProps, + 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 { Journey } from './value-objects/journey.value-object'; import { CalendarTools } from './calendar-tools.service'; 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 { protected readonly _id: AggregateID; @@ -15,8 +25,8 @@ export class CandidateEntity extends AggregateRoot { return new CandidateEntity({ id: create.id, props }); }; - setCarpoolPath = (carpoolSteps: CarpoolStepProps[]): CandidateEntity => { - this.props.carpoolSteps = carpoolSteps; + setCarpoolPath = (carpoolPath: CarpoolPathItemProps[]): CandidateEntity => { + this.props.carpoolPath = carpoolPath; return this; }; @@ -34,21 +44,14 @@ export class CandidateEntity extends AggregateRoot { isDetourValid = (): boolean => this._validateDistanceDetour() && this._validateDurationDetour(); - createJourneys = (fromDate: string, toDate: string): CandidateEntity => { - this.props.driverJourneys = this.props.driverSchedule - .map((driverScheduleItem: ScheduleItem) => - this._createJourney(fromDate, toDate, driverScheduleItem), - ) - .filter( - (journey: Journey | undefined) => journey !== undefined, - ) as Journey[]; - this.props.passengerJourneys = this.props.passengerSchedule - .map((passengerScheduleItem: ScheduleItem) => - this._createJourney(fromDate, toDate, passengerScheduleItem), - ) - .filter( - (journey: Journey | undefined) => journey !== undefined, - ) as Journey[]; + /** + * Create the journeys based on the driver schedule (the driver 'drives' the carpool !) + */ + createJourneys = (): CandidateEntity => { + this.props.journeys = this.props.driverSchedule.map( + (driverScheduleItem: ScheduleItem) => + this._createJourney(driverScheduleItem), + ); return this; }; @@ -66,23 +69,159 @@ export class CandidateEntity extends AggregateRoot { (1 + this.props.spacetimeDetourRatio.maxDistanceDetourRatio) : false; - private _createJourney = ( - fromDate: string, - toDate: string, - scheduleItem: ScheduleItem, - ): Journey | undefined => + private _createJourney = (driverScheduleItem: ScheduleItem): Journey => new Journey({ - day: scheduleItem.day, - firstDate: CalendarTools.firstDate(scheduleItem.day, fromDate, toDate), - lastDate: CalendarTools.lastDate(scheduleItem.day, fromDate, toDate), - journeyItems: this._createJourneyItems(scheduleItem), + firstDate: CalendarTools.firstDate( + driverScheduleItem.day, + this.props.dateInterval, + ), + lastDate: CalendarTools.lastDate( + driverScheduleItem.day, + this.props.dateInterval, + ), + journeyItems: this._createJourneyItems(driverScheduleItem), }); private _createJourneyItems = ( - scheduleItem: ScheduleItem, - ): JourneyItem[] => []; + driverScheduleItem: ScheduleItem, + ): 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) => + { + 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 { // 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; +}; diff --git a/src/modules/ad/core/domain/candidate.types.ts b/src/modules/ad/core/domain/candidate.types.ts index 07b5d27..c9bc237 100644 --- a/src/modules/ad/core/domain/candidate.types.ts +++ b/src/modules/ad/core/domain/candidate.types.ts @@ -1,7 +1,7 @@ import { Role } from './ad.types'; import { PointProps } from './value-objects/point.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 { StepProps } from './value-objects/step.value-object'; @@ -10,16 +10,16 @@ export interface CandidateProps { role: Role; driverWaypoints: PointProps[]; passengerWaypoints: PointProps[]; + driverSchedule: ScheduleItemProps[]; + passengerSchedule: ScheduleItemProps[]; driverDistance: number; driverDuration: number; - carpoolSteps?: CarpoolStepProps[]; + dateInterval: DateInterval; + carpoolPath?: CarpoolPathItemProps[]; distance?: number; duration?: number; steps?: StepProps[]; - driverSchedule: ScheduleItemProps[]; - passengerSchedule: ScheduleItemProps[]; - driverJourneys?: JourneyProps[]; - passengerJourneys?: JourneyProps[]; + journeys?: JourneyProps[]; spacetimeDetourRatio: SpacetimeDetourRatio; } @@ -34,6 +34,7 @@ export interface CreateCandidateProps { driverSchedule: ScheduleItemProps[]; passengerSchedule: ScheduleItemProps[]; spacetimeDetourRatio: SpacetimeDetourRatio; + dateInterval: DateInterval; } export enum Target { @@ -47,12 +48,12 @@ export abstract class Validator { abstract validate(): boolean; } -export type SpacetimeMetric = { - distance: number; - duration: number; -}; - export type SpacetimeDetourRatio = { maxDistanceDetourRatio: number; maxDurationDetourRatio: number; }; + +export type DateInterval = { + lowerDate: string; + higherDate: string; +}; diff --git a/src/modules/ad/core/domain/carpool-path-creator.service.ts b/src/modules/ad/core/domain/carpool-path-creator.service.ts index 68a0a51..5448ae2 100644 --- a/src/modules/ad/core/domain/carpool-path-creator.service.ts +++ b/src/modules/ad/core/domain/carpool-path-creator.service.ts @@ -3,7 +3,7 @@ import { Target } from './candidate.types'; import { CarpoolPathCreatorException } from './match.errors'; import { Actor } from './value-objects/actor.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 { 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 of the driver waypoints Inspired by : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment */ - public carpoolPath = (): CarpoolStep[] => + public carpoolPath = (): CarpoolPathItem[] => this._consolidate( - this._mixedCarpoolSteps( - this._driverCarpoolSteps(), - this._passengerCarpoolSteps(), + this._mixedCarpoolPath( + this._driverCarpoolPath(), + this._passengerCarpoolPath(), ), ); - private _mixedCarpoolSteps = ( - driverCarpoolSteps: CarpoolStep[], - passengerCarpoolSteps: CarpoolStep[], - ): CarpoolStep[] => - driverCarpoolSteps.length == 2 - ? this._simpleMixedCarpoolSteps(driverCarpoolSteps, passengerCarpoolSteps) - : this._complexMixedCarpoolSteps( - driverCarpoolSteps, - passengerCarpoolSteps, - ); + private _mixedCarpoolPath = ( + driverCarpoolPath: CarpoolPathItem[], + passengerCarpoolPath: CarpoolPathItem[], + ): CarpoolPathItem[] => + driverCarpoolPath.length == 2 + ? this._simpleMixedCarpoolPath(driverCarpoolPath, passengerCarpoolPath) + : this._complexMixedCarpoolPath(driverCarpoolPath, passengerCarpoolPath); - private _driverCarpoolSteps = (): CarpoolStep[] => + private _driverCarpoolPath = (): CarpoolPathItem[] => this.driverWaypoints.map( (waypoint: Point, index: number) => - new CarpoolStep({ - point: new Point({ - lon: waypoint.lon, - lat: waypoint.lat, - }), + new CarpoolPathItem({ + lon: waypoint.lon, + lat: waypoint.lat, actors: [ new Actor({ 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[] => { - const carpoolSteps: CarpoolStep[] = []; + private _passengerCarpoolPath = (): CarpoolPathItem[] => { + const carpoolPath: CarpoolPathItem[] = []; this.passengerWaypoints.forEach( (passengerWaypoint: Point, index: number) => { - const carpoolStep: CarpoolStep = new CarpoolStep({ - point: new Point({ - lon: passengerWaypoint.lon, - lat: passengerWaypoint.lat, - }), + const carpoolPathItem: CarpoolPathItem = new CarpoolPathItem({ + lon: passengerWaypoint.lon, + lat: passengerWaypoint.lat, actors: [ new Actor({ role: Role.PASSENGER, @@ -89,78 +82,80 @@ export class CarpoolPathCreator { passengerWaypoint.equals(driverWaypoint), ).length == 0 ) { - carpoolStep.actors.push( + carpoolPathItem.actors.push( new Actor({ role: Role.DRIVER, target: Target.NEUTRAL, }), ); } - carpoolSteps.push(carpoolStep); + carpoolPath.push(carpoolPathItem); }, ); - return carpoolSteps; + return carpoolPath; }; - private _simpleMixedCarpoolSteps = ( - driverCarpoolSteps: CarpoolStep[], - passengerCarpoolSteps: CarpoolStep[], - ): CarpoolStep[] => [ - driverCarpoolSteps[0], - ...passengerCarpoolSteps, - driverCarpoolSteps[1], + private _simpleMixedCarpoolPath = ( + driverCarpoolPath: CarpoolPathItem[], + passengerCarpoolPath: CarpoolPathItem[], + ): CarpoolPathItem[] => [ + driverCarpoolPath[0], + ...passengerCarpoolPath, + driverCarpoolPath[1], ]; - private _complexMixedCarpoolSteps = ( - driverCarpoolSteps: CarpoolStep[], - passengerCarpoolSteps: CarpoolStep[], - ): CarpoolStep[] => { - let mixedCarpoolSteps: CarpoolStep[] = [...driverCarpoolSteps]; + private _complexMixedCarpoolPath = ( + driverCarpoolPath: CarpoolPathItem[], + passengerCarpoolPath: CarpoolPathItem[], + ): CarpoolPathItem[] => { + let mixedCarpoolPath: CarpoolPathItem[] = [...driverCarpoolPath]; const originInsertIndex: number = this._insertIndex( - passengerCarpoolSteps[0], - driverCarpoolSteps, + passengerCarpoolPath[0], + driverCarpoolPath, ); - mixedCarpoolSteps = [ - ...mixedCarpoolSteps.slice(0, originInsertIndex), - passengerCarpoolSteps[0], - ...mixedCarpoolSteps.slice(originInsertIndex), + mixedCarpoolPath = [ + ...mixedCarpoolPath.slice(0, originInsertIndex), + passengerCarpoolPath[0], + ...mixedCarpoolPath.slice(originInsertIndex), ]; const destinationInsertIndex: number = this._insertIndex( - passengerCarpoolSteps[passengerCarpoolSteps.length - 1], - driverCarpoolSteps, + passengerCarpoolPath[passengerCarpoolPath.length - 1], + driverCarpoolPath, ) + 1; - mixedCarpoolSteps = [ - ...mixedCarpoolSteps.slice(0, destinationInsertIndex), - passengerCarpoolSteps[passengerCarpoolSteps.length - 1], - ...mixedCarpoolSteps.slice(destinationInsertIndex), + mixedCarpoolPath = [ + ...mixedCarpoolPath.slice(0, destinationInsertIndex), + passengerCarpoolPath[passengerCarpoolPath.length - 1], + ...mixedCarpoolPath.slice(destinationInsertIndex), ]; - return mixedCarpoolSteps; + return mixedCarpoolPath; }; private _insertIndex = ( - targetCarpoolStep: CarpoolStep, - carpoolSteps: CarpoolStep[], + targetCarpoolPathItem: CarpoolPathItem, + carpoolPath: CarpoolPathItem[], ): number => - this._closestSegmentIndex(targetCarpoolStep, this._segments(carpoolSteps)) + - 1; + this._closestSegmentIndex( + targetCarpoolPathItem, + this._segments(carpoolPath), + ) + 1; - private _segments = (carpoolSteps: CarpoolStep[]): CarpoolStep[][] => { - const segments: CarpoolStep[][] = []; - carpoolSteps.forEach((carpoolStep: CarpoolStep, index: number) => { - if (index < carpoolSteps.length - 1) - segments.push([carpoolStep, carpoolSteps[index + 1]]); + private _segments = (carpoolPath: CarpoolPathItem[]): CarpoolPathItem[][] => { + const segments: CarpoolPathItem[][] = []; + carpoolPath.forEach((carpoolPathItem: CarpoolPathItem, index: number) => { + if (index < carpoolPath.length - 1) + segments.push([carpoolPathItem, carpoolPath[index + 1]]); }); return segments; }; private _closestSegmentIndex = ( - carpoolStep: CarpoolStep, - segments: CarpoolStep[][], + carpoolPathItem: CarpoolPathItem, + segments: CarpoolPathItem[][], ): number => { const distances: Map = new Map(); - segments.forEach((segment: CarpoolStep[], index: number) => { - distances.set(index, this._distanceToSegment(carpoolStep, segment)); + segments.forEach((segment: CarpoolPathItem[], index: number) => { + distances.set(index, this._distanceToSegment(carpoolPathItem, segment)); }); const sortedDistances: Map = new Map( [...distances.entries()].sort((a, b) => a[1] - b[1]), @@ -170,45 +165,62 @@ export class CarpoolPathCreator { }; private _distanceToSegment = ( - carpoolStep: CarpoolStep, - segment: CarpoolStep[], + carpoolPathItem: CarpoolPathItem, + segment: CarpoolPathItem[], ): number => parseFloat( - Math.sqrt(this._distanceToSegmentSquared(carpoolStep, segment)).toFixed( - this.PRECISION, - ), + Math.sqrt( + this._distanceToSegmentSquared(carpoolPathItem, segment), + ).toFixed(this.PRECISION), ); private _distanceToSegmentSquared = ( - carpoolStep: CarpoolStep, - segment: CarpoolStep[], + carpoolPathItem: CarpoolPathItem, + segment: CarpoolPathItem[], ): number => { const length2: number = this._distanceSquared( - segment[0].point, - segment[1].point, + new Point({ + lon: segment[0].lon, + lat: segment[0].lat, + }), + new Point({ + lon: segment[1].lon, + lat: segment[1].lat, + }), ); 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( 0, Math.min( 1, - ((carpoolStep.point.lon - segment[0].point.lon) * - (segment[1].point.lon - segment[0].point.lon) + - (carpoolStep.point.lat - segment[0].point.lat) * - (segment[1].point.lat - segment[0].point.lat)) / + ((carpoolPathItem.lon - segment[0].lon) * + (segment[1].lon - segment[0].lon) + + (carpoolPathItem.lat - segment[0].lat) * + (segment[1].lat - segment[0].lat)) / length2, ), ); const newPoint: Point = new Point({ - lon: - segment[0].point.lon + - length * (segment[1].point.lon - segment[0].point.lon), - lat: - segment[0].point.lat + - length * (segment[1].point.lat - segment[0].point.lat), + lon: segment[0].lon + length * (segment[1].lon - segment[0].lon), + lat: segment[0].lat + length * (segment[1].lat - segment[0].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 => @@ -227,31 +239,43 @@ export class CarpoolPathCreator { : 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[] = []; - carpoolSteps.forEach((carpoolStep: CarpoolStep) => { + carpoolPath.forEach((carpoolPathItem: CarpoolPathItem) => { if ( - uniquePoints.find((point: Point) => point.equals(carpoolStep.point)) === - undefined + uniquePoints.find((point: Point) => + point.equals( + new Point({ + lon: carpoolPathItem.lon, + lat: carpoolPathItem.lat, + }), + ), + ) === undefined ) uniquePoints.push( new Point({ - lon: carpoolStep.point.lon, - lat: carpoolStep.point.lat, + lon: carpoolPathItem.lon, + lat: carpoolPathItem.lat, }), ); }); return uniquePoints.map( (point: Point) => - new CarpoolStep({ - point, - actors: carpoolSteps - .filter((carpoolStep: CarpoolStep) => - carpoolStep.point.equals(point), + new CarpoolPathItem({ + lon: point.lon, + lat: point.lat, + actors: carpoolPath + .filter((carpoolPathItem: CarpoolPathItem) => + new Point({ + lon: carpoolPathItem.lon, + lat: carpoolPathItem.lat, + }).equals(point), ) - .map((carpoolStep: CarpoolStep) => carpoolStep.actors) + .map((carpoolPathItem: CarpoolPathItem) => carpoolPathItem.actors) .flat(), }), ); diff --git a/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts b/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts index 9702dcb..41b0bfa 100644 --- a/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts @@ -56,7 +56,7 @@ export class ActorTime extends ValueObject { role: props.role, target: props.target, }); - if (props.firstDatetime.getDay() != props.lastDatetime.getDay()) + if (props.firstDatetime.getUTCDay() != props.lastDatetime.getUTCDay()) throw new ArgumentInvalidException( 'firstDatetime week day must be equal to lastDatetime week day', ); diff --git a/src/modules/ad/core/domain/value-objects/carpool-step.value-object.ts b/src/modules/ad/core/domain/value-objects/carpool-path-item.value-object.ts similarity index 61% rename from src/modules/ad/core/domain/value-objects/carpool-step.value-object.ts rename to src/modules/ad/core/domain/value-objects/carpool-path-item.value-object.ts index 95382cf..bb7dafa 100644 --- a/src/modules/ad/core/domain/value-objects/carpool-step.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/carpool-path-item.value-object.ts @@ -4,28 +4,36 @@ import { } from '@mobicoop/ddd-library'; import { Actor } from './actor.value-object'; import { Role } from '../ad.types'; -import { Point } from './point.value-object'; +import { Point, PointProps } from './point.value-object'; /** Note: * Value Objects with multiple properties can contain * other Value Objects inside if needed. * */ -export interface CarpoolStepProps { - point: Point; +export interface CarpoolPathItemProps extends PointProps { actors: Actor[]; } -export class CarpoolStep extends ValueObject { - get point(): Point { - return this.props.point; +export class CarpoolPathItem extends ValueObject { + get lon(): number { + return this.props.lon; + } + + get lat(): number { + return this.props.lat; } get actors(): Actor[] { 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) throw new ArgumentOutOfRangeException('at least one actor is required'); if ( diff --git a/src/modules/ad/core/domain/value-objects/journey.value-object.ts b/src/modules/ad/core/domain/value-objects/journey.value-object.ts index fb7f421..df03132 100644 --- a/src/modules/ad/core/domain/value-objects/journey.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/journey.value-object.ts @@ -1,8 +1,4 @@ -import { - ArgumentInvalidException, - ArgumentOutOfRangeException, - ValueObject, -} from '@mobicoop/ddd-library'; +import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library'; import { JourneyItem } from './journey-item.value-object'; /** Note: @@ -13,7 +9,6 @@ import { JourneyItem } from './journey-item.value-object'; export interface JourneyProps { firstDate: Date; lastDate: Date; - day: number; journeyItems: JourneyItem[]; } @@ -26,18 +21,12 @@ export class Journey extends ValueObject { return this.props.lastDate; } - get day(): number { - return this.props.day; - } - get journeyItems(): JourneyItem[] { return this.props.journeyItems; } protected validate(props: JourneyProps): void { - if (props.day < 0 || props.day > 6) - throw new ArgumentOutOfRangeException('day must be between 0 and 6'); - if (props.firstDate.getDay() != props.lastDate.getDay()) + if (props.firstDate.getUTCDay() != props.lastDate.getUTCDay()) throw new ArgumentInvalidException( 'firstDate week day must be equal to lastDate week day', ); diff --git a/src/modules/ad/infrastructure/input-datetime-transformer.ts b/src/modules/ad/infrastructure/input-datetime-transformer.ts index 97df366..5181a0a 100644 --- a/src/modules/ad/infrastructure/input-datetime-transformer.ts +++ b/src/modules/ad/infrastructure/input-datetime-transformer.ts @@ -79,7 +79,7 @@ export class InputDateTimeTransformer implements DateTimeTransformerPort { this._defaultTimezone, )[0], ); - return new Date(this.fromDate(geoFromDate, frequency)).getDay(); + return new Date(this.fromDate(geoFromDate, frequency)).getUTCDay(); }; /** diff --git a/src/modules/ad/infrastructure/time-converter.ts b/src/modules/ad/infrastructure/time-converter.ts index bb186de..fc3314f 100644 --- a/src/modules/ad/infrastructure/time-converter.ts +++ b/src/modules/ad/infrastructure/time-converter.ts @@ -45,7 +45,7 @@ export class TimeConverter implements TimeConverterPort { .convert(TimeZone.zone('UTC')) .toIsoString() .split('T')[0], - ).getDay(); + ).getUTCDay(); localUnixEpochDayFromTime = (time: string, timezone: string): number => new Date( @@ -53,5 +53,5 @@ export class TimeConverter implements TimeConverterPort { .convert(TimeZone.zone(timezone)) .toIsoString() .split('T')[0], - ).getDay(); + ).getUTCDay(); } diff --git a/src/modules/ad/tests/unit/core/actor-time.value-object.spec.ts b/src/modules/ad/tests/unit/core/actor-time.value-object.spec.ts index 3d7ed4e..f41ccba 100644 --- a/src/modules/ad/tests/unit/core/actor-time.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/actor-time.value-object.spec.ts @@ -8,21 +8,21 @@ describe('Actor time value object', () => { const actorTimeVO = 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'), + firstDatetime: new Date('2023-09-01T07:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45Z'), + firstMaxDatetime: new Date('2023-09-01T07:15Z'), + lastDatetime: new Date('2024-08-30T07:00Z'), + lastMinDatetime: new Date('2024-08-30T06:45Z'), + lastMaxDatetime: new Date('2024-08-30T07:15Z'), }); expect(actorTimeVO.role).toBe(Role.DRIVER); expect(actorTimeVO.target).toBe(Target.START); - expect(actorTimeVO.firstDatetime.getHours()).toBe(7); - expect(actorTimeVO.firstMinDatetime.getMinutes()).toBe(45); - expect(actorTimeVO.firstMaxDatetime.getMinutes()).toBe(15); - expect(actorTimeVO.lastDatetime.getHours()).toBe(7); - expect(actorTimeVO.lastMinDatetime.getMinutes()).toBe(45); - expect(actorTimeVO.lastMaxDatetime.getMinutes()).toBe(15); + expect(actorTimeVO.firstDatetime.getUTCHours()).toBe(7); + expect(actorTimeVO.firstMinDatetime.getUTCMinutes()).toBe(45); + expect(actorTimeVO.firstMaxDatetime.getUTCMinutes()).toBe(15); + expect(actorTimeVO.lastDatetime.getUTCHours()).toBe(7); + expect(actorTimeVO.lastMinDatetime.getUTCMinutes()).toBe(45); + expect(actorTimeVO.lastMaxDatetime.getUTCMinutes()).toBe(15); }); it('should throw an error if dates are inconsistent', () => { expect( @@ -30,12 +30,12 @@ describe('Actor time value object', () => { new ActorTime({ role: Role.DRIVER, target: Target.START, - firstDatetime: new Date('2023-09-01 07:00'), - firstMinDatetime: new Date('2023-09-01 07:05'), - 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'), + firstDatetime: new Date('2023-09-01T07:00Z'), + firstMinDatetime: new Date('2023-09-01T07:05Z'), + firstMaxDatetime: new Date('2023-09-01T07:15Z'), + lastDatetime: new Date('2024-08-30T07:00Z'), + lastMinDatetime: new Date('2024-08-30T06:45Z'), + lastMaxDatetime: new Date('2024-08-30T07:15Z'), }), ).toThrow(ArgumentInvalidException); expect( @@ -43,12 +43,12 @@ describe('Actor time value object', () => { 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 06:55'), - lastDatetime: new Date('2024-08-30 07:00'), - lastMinDatetime: new Date('2024-08-30 06:45'), - lastMaxDatetime: new Date('2024-08-30 07:15'), + firstDatetime: new Date('2023-09-01T07:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45Z'), + firstMaxDatetime: new Date('2023-09-01T06:55Z'), + lastDatetime: new Date('2024-08-30T07:00Z'), + lastMinDatetime: new Date('2024-08-30T06:45Z'), + lastMaxDatetime: new Date('2024-08-30T07:15Z'), }), ).toThrow(ArgumentInvalidException); expect( @@ -56,12 +56,12 @@ describe('Actor time value object', () => { 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 07:05'), - lastMaxDatetime: new Date('2024-08-30 07:15'), + firstDatetime: new Date('2023-09-01T07:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45Z'), + firstMaxDatetime: new Date('2023-09-01T07:15Z'), + lastDatetime: new Date('2024-08-30T07:00Z'), + lastMinDatetime: new Date('2024-08-30T07:05Z'), + lastMaxDatetime: new Date('2024-08-30T07:15Z'), }), ).toThrow(ArgumentInvalidException); expect( @@ -69,12 +69,12 @@ describe('Actor time value object', () => { 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 06:35'), + firstDatetime: new Date('2023-09-01T07:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45Z'), + firstMaxDatetime: new Date('2023-09-01T07:15Z'), + lastDatetime: new Date('2024-08-30T07:00Z'), + lastMinDatetime: new Date('2024-08-30T06:45Z'), + lastMaxDatetime: new Date('2024-08-30T06:35Z'), }), ).toThrow(ArgumentInvalidException); expect( @@ -82,12 +82,12 @@ describe('Actor time value object', () => { new ActorTime({ role: Role.DRIVER, target: Target.START, - firstDatetime: new Date('2024-08-30 07:00'), - firstMinDatetime: new Date('2024-08-30 06:45'), - firstMaxDatetime: new Date('2024-08-30 07:15'), - lastDatetime: new Date('2023-09-01 07:00'), - lastMinDatetime: new Date('2023-09-01 06:45'), - lastMaxDatetime: new Date('2023-09-01 07:15'), + firstDatetime: new Date('2024-08-30T07:00Z'), + firstMinDatetime: new Date('2024-08-30T06:45Z'), + firstMaxDatetime: new Date('2024-08-30T07:15Z'), + lastDatetime: new Date('2023-09-01T07:00Z'), + lastMinDatetime: new Date('2023-09-01T06:45Z'), + lastMaxDatetime: new Date('2023-09-01T07:15Z'), }), ).toThrow(ArgumentInvalidException); expect( @@ -95,12 +95,12 @@ describe('Actor time value object', () => { 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-31 07:00'), - lastMinDatetime: new Date('2024-08-31 06:45'), - lastMaxDatetime: new Date('2024-08-31 06:35'), + firstDatetime: new Date('2023-09-01T07:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45Z'), + firstMaxDatetime: new Date('2023-09-01T07:15Z'), + lastDatetime: new Date('2024-08-31T07:00Z'), + lastMinDatetime: new Date('2024-08-31T06:45Z'), + lastMaxDatetime: new Date('2024-08-31T06:35Z'), }), ).toThrow(ArgumentInvalidException); }); diff --git a/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts b/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts index c9757ce..720d5b6 100644 --- a/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts +++ b/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts @@ -67,6 +67,10 @@ class SomeSelector extends Selector { CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', + }, driverWaypoints: [ { lat: 48.678454, diff --git a/src/modules/ad/tests/unit/core/calendar-tools.service.spec.ts b/src/modules/ad/tests/unit/core/calendar-tools.service.spec.ts index a31ff87..9dccb04 100644 --- a/src/modules/ad/tests/unit/core/calendar-tools.service.spec.ts +++ b/src/modules/ad/tests/unit/core/calendar-tools.service.spec.ts @@ -6,78 +6,175 @@ import { describe('Calendar tools service', () => { describe('First date', () => { it('should return the first date for a given week day within a date range', () => { - const firstDate: Date = CalendarTools.firstDate( - 1, - '2023-08-31', - '2023-09-07', - ); - expect(firstDate.getDay()).toBe(1); - expect(firstDate.getDate()).toBe(4); - expect(firstDate.getMonth()).toBe(8); - const secondDate: Date = CalendarTools.firstDate( - 5, - '2023-08-31', - '2023-09-07', - ); - expect(secondDate.getDay()).toBe(5); - expect(secondDate.getDate()).toBe(1); - expect(secondDate.getMonth()).toBe(8); - const thirdDate: Date = CalendarTools.firstDate( - 4, - '2023-08-31', - '2023-09-07', - ); - expect(thirdDate.getDay()).toBe(4); - expect(thirdDate.getDate()).toBe(31); - expect(thirdDate.getMonth()).toBe(7); + const firstDate: Date = CalendarTools.firstDate(1, { + lowerDate: '2023-08-31', + higherDate: '2023-09-07', + }); + expect(firstDate.getUTCDay()).toBe(1); + expect(firstDate.getUTCDate()).toBe(4); + expect(firstDate.getUTCMonth()).toBe(8); + const secondDate: Date = CalendarTools.firstDate(5, { + lowerDate: '2023-08-31', + higherDate: '2023-09-07', + }); + expect(secondDate.getUTCDay()).toBe(5); + expect(secondDate.getUTCDate()).toBe(1); + expect(secondDate.getUTCMonth()).toBe(8); + const thirdDate: Date = CalendarTools.firstDate(4, { + lowerDate: '2023-08-31', + higherDate: '2023-09-07', + }); + expect(thirdDate.getUTCDay()).toBe(4); + expect(thirdDate.getUTCDate()).toBe(31); + expect(thirdDate.getUTCMonth()).toBe(7); }); it('should throw an exception if a given week day is not within a date range', () => { expect(() => { - CalendarTools.firstDate(1, '2023-09-05', '2023-09-07'); + CalendarTools.firstDate(1, { + lowerDate: '2023-09-05', + higherDate: '2023-09-07', + }); }).toThrow(CalendarToolsException); }); it('should throw an exception if a given week day is invalid', () => { expect(() => { - CalendarTools.firstDate(8, '2023-09-05', '2023-09-07'); + CalendarTools.firstDate(8, { + lowerDate: '2023-09-05', + higherDate: '2023-09-07', + }); }).toThrow(CalendarToolsException); }); }); describe('Second date', () => { it('should return the last date for a given week day within a date range', () => { - const firstDate: Date = CalendarTools.lastDate( - 0, - '2023-09-30', - '2024-09-30', - ); - expect(firstDate.getDay()).toBe(0); - expect(firstDate.getDate()).toBe(29); - expect(firstDate.getMonth()).toBe(8); - const secondDate: Date = CalendarTools.lastDate( - 5, - '2023-09-30', - '2024-09-30', - ); - expect(secondDate.getDay()).toBe(5); - expect(secondDate.getDate()).toBe(27); - expect(secondDate.getMonth()).toBe(8); - const thirdDate: Date = CalendarTools.lastDate( - 1, - '2023-09-30', - '2024-09-30', - ); - expect(thirdDate.getDay()).toBe(1); - expect(thirdDate.getDate()).toBe(30); - expect(thirdDate.getMonth()).toBe(8); + const firstDate: Date = CalendarTools.lastDate(0, { + lowerDate: '2023-09-30', + higherDate: '2024-09-30', + }); + expect(firstDate.getUTCDay()).toBe(0); + expect(firstDate.getUTCDate()).toBe(29); + expect(firstDate.getUTCMonth()).toBe(8); + const secondDate: Date = CalendarTools.lastDate(5, { + lowerDate: '2023-09-30', + higherDate: '2024-09-30', + }); + expect(secondDate.getUTCDay()).toBe(5); + expect(secondDate.getUTCDate()).toBe(27); + expect(secondDate.getUTCMonth()).toBe(8); + const thirdDate: Date = CalendarTools.lastDate(1, { + lowerDate: '2023-09-30', + higherDate: '2024-09-30', + }); + expect(thirdDate.getUTCDay()).toBe(1); + expect(thirdDate.getUTCDate()).toBe(30); + expect(thirdDate.getUTCMonth()).toBe(8); }); it('should throw an exception if a given week day is not within a date range', () => { expect(() => { - CalendarTools.lastDate(2, '2024-09-27', '2024-09-30'); + CalendarTools.lastDate(2, { + lowerDate: '2024-09-27', + higherDate: '2024-09-30', + }); }).toThrow(CalendarToolsException); }); it('should throw an exception if a given week day is invalid', () => { 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); }); }); diff --git a/src/modules/ad/tests/unit/core/candidate.entity.spec.ts b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts index c70d573..9556e10 100644 --- a/src/modules/ad/tests/unit/core/candidate.entity.spec.ts +++ b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts @@ -2,13 +2,16 @@ import { Role } from '@modules/ad/core/domain/ad.types'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Target } from '@modules/ad/core/domain/candidate.types'; import { 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', () => { it('should create a new candidate entity', () => { const candidateEntity: 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, @@ -52,10 +55,15 @@ describe('Candidate entity', () => { }); expect(candidateEntity.id.length).toBe(36); }); + it('should set a candidate entity carpool path', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', + }, driverWaypoints: [ { lat: 48.689445, @@ -98,10 +106,8 @@ describe('Candidate entity', () => { }, }).setCarpoolPath([ { - point: new Point({ - lat: 48.689445, - lon: 6.17651, - }), + lat: 48.689445, + lon: 6.17651, actors: [ new Actor({ role: Role.DRIVER, @@ -114,10 +120,8 @@ describe('Candidate entity', () => { ], }, { - point: new Point({ - lat: 48.8566, - lon: 2.3522, - }), + lat: 48.8566, + lon: 2.3522, actors: [ new Actor({ 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', () => { const candidateEntity: 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, @@ -180,98 +189,113 @@ describe('Candidate entity', () => { expect(candidateEntity.getProps().distance).toBe(352688); expect(candidateEntity.getProps().duration).toBe(14587); }); - it('should not validate a candidate entity with exceeding distance detour', () => { - const candidateEntity: CandidateEntity = CandidateEntity.create({ - id: 'cc260669-1c6d-441f-80a5-19cd59afb777', - role: Role.DRIVER, - driverWaypoints: [ - { - lat: 48.678454, - lon: 6.189745, + + describe('detour validation', () => { + it('should not validate a candidate entity with exceeding distance detour', () => { + const candidateEntity: CandidateEntity = CandidateEntity.create({ + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + role: Role.DRIVER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', }, - { - lat: 48.84877, - lon: 2.398457, + driverWaypoints: [ + { + lat: 48.678454, + lon: 6.189745, + }, + { + lat: 48.84877, + lon: 2.398457, + }, + ], + passengerWaypoints: [ + { + lat: 48.849445, + lon: 6.68651, + }, + { + lat: 47.18746, + lon: 2.89742, + }, + ], + 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, }, - ], - passengerWaypoints: [ - { - lat: 48.849445, - lon: 6.68651, + }).setMetrics(458690, 13980); + expect(candidateEntity.isDetourValid()).toBeFalsy(); + }); + it('should not validate a candidate entity with exceeding duration detour', () => { + const candidateEntity: CandidateEntity = CandidateEntity.create({ + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + role: Role.DRIVER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', }, - { - lat: 47.18746, - lon: 2.89742, + driverWaypoints: [ + { + lat: 48.678454, + lon: 6.189745, + }, + { + lat: 48.84877, + lon: 2.398457, + }, + ], + passengerWaypoints: [ + { + lat: 48.849445, + lon: 6.68651, + }, + { + lat: 47.18746, + lon: 2.89742, + }, + ], + 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, }, - ], - 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, - }, - }).setMetrics(458690, 13980); - expect(candidateEntity.isDetourValid()).toBeFalsy(); + }).setMetrics(352368, 18314); + expect(candidateEntity.isDetourValid()).toBeFalsy(); + }); }); - it('should not validate a candidate entity with exceeding duration detour', () => { - const candidateEntity: CandidateEntity = CandidateEntity.create({ - id: 'cc260669-1c6d-441f-80a5-19cd59afb777', - role: Role.DRIVER, - driverWaypoints: [ - { - lat: 48.678454, - lon: 6.189745, - }, - { - lat: 48.84877, - lon: 2.398457, - }, - ], - passengerWaypoints: [ - { - lat: 48.849445, - lon: 6.68651, - }, - { - lat: 47.18746, - lon: 2.89742, - }, - ], - 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, - }, - }).setMetrics(352368, 18314); - expect(candidateEntity.isDetourValid()).toBeFalsy(); + + describe('Journeys', () => { + it('should create journeys', () => {}); }); }); diff --git a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts index 8fbb643..709c2bf 100644 --- a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts +++ b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts @@ -1,7 +1,7 @@ import { CarpoolPathCreator } from '@modules/ad/core/domain/carpool-path-creator.service'; import { CarpoolPathCreatorException } from '@modules/ad/core/domain/match.errors'; 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({ lat: 0, @@ -34,71 +34,71 @@ describe('Carpool Path Creator Service', () => { [waypoint1, waypoint6], [waypoint2, waypoint5], ); - const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); - expect(carpoolSteps).toHaveLength(4); - expect(carpoolSteps[0].actors.length).toBe(1); + const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath(); + expect(carpoolPath).toHaveLength(4); + expect(carpoolPath[0].actors.length).toBe(1); }); it('should create a simple carpool path with same destination for driver and passenger', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint6], [waypoint2, waypoint6], ); - const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); - expect(carpoolSteps).toHaveLength(3); - expect(carpoolSteps[0].actors.length).toBe(1); - expect(carpoolSteps[1].actors.length).toBe(2); - expect(carpoolSteps[2].actors.length).toBe(2); + const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath(); + expect(carpoolPath).toHaveLength(3); + expect(carpoolPath[0].actors.length).toBe(1); + expect(carpoolPath[1].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', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint6], [waypoint1, waypoint6], ); - const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); - expect(carpoolSteps).toHaveLength(2); - expect(carpoolSteps[0].actors.length).toBe(2); - expect(carpoolSteps[1].actors.length).toBe(2); + const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath(); + expect(carpoolPath).toHaveLength(2); + expect(carpoolPath[0].actors.length).toBe(2); + expect(carpoolPath[1].actors.length).toBe(2); }); it('should create a complex carpool path with 3 driver waypoints', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint3, waypoint6], [waypoint2, waypoint5], ); - const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); - expect(carpoolSteps).toHaveLength(5); - expect(carpoolSteps[0].actors.length).toBe(1); - expect(carpoolSteps[1].actors.length).toBe(2); - expect(carpoolSteps[2].actors.length).toBe(1); - expect(carpoolSteps[3].actors.length).toBe(2); - expect(carpoolSteps[4].actors.length).toBe(1); + const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath(); + expect(carpoolPath).toHaveLength(5); + expect(carpoolPath[0].actors.length).toBe(1); + expect(carpoolPath[1].actors.length).toBe(2); + expect(carpoolPath[2].actors.length).toBe(1); + expect(carpoolPath[3].actors.length).toBe(2); + expect(carpoolPath[4].actors.length).toBe(1); }); it('should create a complex carpool path with 4 driver waypoints', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint3, waypoint4, waypoint6], [waypoint2, waypoint5], ); - const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); - expect(carpoolSteps).toHaveLength(6); - expect(carpoolSteps[0].actors.length).toBe(1); - expect(carpoolSteps[1].actors.length).toBe(2); - expect(carpoolSteps[2].actors.length).toBe(1); - expect(carpoolSteps[3].actors.length).toBe(1); - expect(carpoolSteps[4].actors.length).toBe(2); - expect(carpoolSteps[5].actors.length).toBe(1); + const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath(); + expect(carpoolPath).toHaveLength(6); + expect(carpoolPath[0].actors.length).toBe(1); + expect(carpoolPath[1].actors.length).toBe(2); + expect(carpoolPath[2].actors.length).toBe(1); + expect(carpoolPath[3].actors.length).toBe(1); + expect(carpoolPath[4].actors.length).toBe(2); + expect(carpoolPath[5].actors.length).toBe(1); }); it('should create a alternate complex carpool path with 4 driver waypoints', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint2, waypoint5, waypoint6], [waypoint3, waypoint4], ); - const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); - expect(carpoolSteps).toHaveLength(6); - expect(carpoolSteps[0].actors.length).toBe(1); - expect(carpoolSteps[1].actors.length).toBe(1); - expect(carpoolSteps[2].actors.length).toBe(2); - expect(carpoolSteps[3].actors.length).toBe(2); - expect(carpoolSteps[4].actors.length).toBe(1); - expect(carpoolSteps[5].actors.length).toBe(1); + const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath(); + expect(carpoolPath).toHaveLength(6); + expect(carpoolPath[0].actors.length).toBe(1); + expect(carpoolPath[1].actors.length).toBe(1); + expect(carpoolPath[2].actors.length).toBe(2); + expect(carpoolPath[3].actors.length).toBe(2); + expect(carpoolPath[4].actors.length).toBe(1); + expect(carpoolPath[5].actors.length).toBe(1); }); it('should throw an exception if less than 2 driver waypoints are given', () => { expect(() => { diff --git a/src/modules/ad/tests/unit/core/carpool-step.value-object.spec.ts b/src/modules/ad/tests/unit/core/carpool-path-item.value-object.spec.ts similarity index 60% rename from src/modules/ad/tests/unit/core/carpool-step.value-object.spec.ts rename to src/modules/ad/tests/unit/core/carpool-path-item.value-object.spec.ts index d633079..9da4378 100644 --- a/src/modules/ad/tests/unit/core/carpool-step.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/carpool-path-item.value-object.spec.ts @@ -2,16 +2,13 @@ import { ArgumentOutOfRangeException } from '@mobicoop/ddd-library'; import { Role } from '@modules/ad/core/domain/ad.types'; import { Target } from '@modules/ad/core/domain/candidate.types'; 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 { 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'; -describe('CarpoolStep value object', () => { - it('should create a carpoolStep value object', () => { - const carpoolStepVO = new CarpoolStep({ - point: new Point({ - lat: 48.689445, - lon: 6.17651, - }), +describe('Carpool Path Item value object', () => { + it('should create a path item value object', () => { + const carpoolPathItemVO = new CarpoolPathItem({ + lat: 48.689445, + lon: 6.17651, actors: [ new Actor({ role: Role.DRIVER, @@ -23,28 +20,24 @@ describe('CarpoolStep value object', () => { }), ], }); - expect(carpoolStepVO.point.lon).toBe(6.17651); - expect(carpoolStepVO.point.lat).toBe(48.689445); - expect(carpoolStepVO.actors).toHaveLength(2); + expect(carpoolPathItemVO.lon).toBe(6.17651); + expect(carpoolPathItemVO.lat).toBe(48.689445); + expect(carpoolPathItemVO.actors).toHaveLength(2); }); it('should throw an exception if actors is empty', () => { expect(() => { - new CarpoolStep({ - point: new Point({ - lat: 48.689445, - lon: 6.17651, - }), + new CarpoolPathItem({ + lat: 48.689445, + lon: 6.17651, actors: [], }); }).toThrow(ArgumentOutOfRangeException); }); it('should throw an exception if actors contains more than one driver', () => { expect(() => { - new CarpoolStep({ - point: new Point({ - lat: 48.689445, - lon: 6.17651, - }), + new CarpoolPathItem({ + lat: 48.689445, + lon: 6.17651, actors: [ new Actor({ role: Role.DRIVER, diff --git a/src/modules/ad/tests/unit/core/journey-item.value-object.spec.ts b/src/modules/ad/tests/unit/core/journey-item.value-object.spec.ts index c4461c1..5c4322e 100644 --- a/src/modules/ad/tests/unit/core/journey-item.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/journey-item.value-object.spec.ts @@ -28,7 +28,9 @@ describe('Journey item value object', () => { expect(journeyItemVO.distance).toBe(48754); expect(journeyItemVO.lon).toBe(6.17651); 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', () => { expect( diff --git a/src/modules/ad/tests/unit/core/journey.completer.spec.ts b/src/modules/ad/tests/unit/core/journey.completer.spec.ts index de6dfd9..ee9d348 100644 --- a/src/modules/ad/tests/unit/core/journey.completer.spec.ts +++ b/src/modules/ad/tests/unit/core/journey.completer.spec.ts @@ -6,7 +6,6 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Target } from '@modules/ad/core/domain/candidate.types'; import { 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 = { position: 0, @@ -66,6 +65,10 @@ const matchQuery = new MatchQuery( 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, @@ -108,10 +111,8 @@ const candidate: CandidateEntity = CandidateEntity.create({ }, }).setCarpoolPath([ { - point: new Point({ - lat: 48.689445, - lon: 6.17651, - }), + lat: 48.689445, + lon: 6.17651, actors: [ new Actor({ role: Role.DRIVER, @@ -124,10 +125,8 @@ const candidate: CandidateEntity = CandidateEntity.create({ ], }, { - point: new Point({ - lat: 48.8566, - lon: 2.3522, - }), + lat: 48.8566, + lon: 2.3522, actors: [ new Actor({ role: Role.DRIVER, diff --git a/src/modules/ad/tests/unit/core/journey.value-object.spec.ts b/src/modules/ad/tests/unit/core/journey.value-object.spec.ts index 3d321e5..6bc1c5e 100644 --- a/src/modules/ad/tests/unit/core/journey.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/journey.value-object.spec.ts @@ -1,7 +1,4 @@ -import { - ArgumentInvalidException, - ArgumentOutOfRangeException, -} from '@mobicoop/ddd-library'; +import { ArgumentInvalidException } from '@mobicoop/ddd-library'; import { Role } from '@modules/ad/core/domain/ad.types'; import { Target } from '@modules/ad/core/domain/candidate.types'; 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({ firstDate: new Date('2023-09-01'), lastDate: new Date('2024-08-30'), - day: 5, journeyItems: [ new JourneyItem({ lat: 48.689445, @@ -109,114 +105,9 @@ describe('Journey value object', () => { }), ], }); - expect(journeyVO.day).toBe(5); expect(journeyVO.journeyItems).toHaveLength(4); - expect(journeyVO.firstDate.getDate()).toBe(1); - expect(journeyVO.lastDate.getMonth()).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); + expect(journeyVO.firstDate.getUTCDate()).toBe(1); + expect(journeyVO.lastDate.getUTCMonth()).toBe(7); }); it('should throw an error if dates are inconsistent', () => { expect( @@ -224,7 +115,6 @@ describe('Journey value object', () => { new Journey({ firstDate: new Date('2023-09-01'), lastDate: new Date('2024-08-31'), - day: 5, journeyItems: [ new JourneyItem({ lat: 48.689445, @@ -326,7 +216,6 @@ describe('Journey value object', () => { new Journey({ firstDate: new Date('2024-08-30'), lastDate: new Date('2023-09-01'), - day: 5, journeyItems: [ new JourneyItem({ lat: 48.689445, @@ -430,7 +319,6 @@ describe('Journey value object', () => { new Journey({ firstDate: new Date('2023-09-01'), lastDate: new Date('2024-08-30'), - day: 5, journeyItems: [ new JourneyItem({ lat: 48.689445, diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts index a7f0a90..7a33fa1 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts @@ -50,6 +50,10 @@ const candidates: 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, @@ -94,6 +98,10 @@ const candidates: CandidateEntity[] = [ CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', + }, driverWaypoints: [ { lat: 48.689445, diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts index 1155369..8bb2344 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts @@ -49,6 +49,10 @@ const matchQuery = new MatchQuery( 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, diff --git a/src/modules/ad/tests/unit/core/route.completer.spec.ts b/src/modules/ad/tests/unit/core/route.completer.spec.ts index 80291da..8daaf47 100644 --- a/src/modules/ad/tests/unit/core/route.completer.spec.ts +++ b/src/modules/ad/tests/unit/core/route.completer.spec.ts @@ -9,7 +9,6 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Target } from '@modules/ad/core/domain/candidate.types'; import { 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 = { position: 0, @@ -70,6 +69,10 @@ const matchQuery = new MatchQuery( 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, @@ -112,10 +115,8 @@ const candidate: CandidateEntity = CandidateEntity.create({ }, }).setCarpoolPath([ { - point: new Point({ - lat: 48.689445, - lon: 6.17651, - }), + lat: 48.689445, + lon: 6.17651, actors: [ new Actor({ role: Role.DRIVER, @@ -128,10 +129,8 @@ const candidate: CandidateEntity = CandidateEntity.create({ ], }, { - point: new Point({ - lat: 48.8566, - lon: 2.3522, - }), + lat: 48.8566, + lon: 2.3522, actors: [ new Actor({ role: Role.DRIVER, diff --git a/src/modules/geography/infrastructure/graphhopper-georouter.ts b/src/modules/geography/infrastructure/graphhopper-georouter.ts index a12b526..635ac3f 100644 --- a/src/modules/geography/infrastructure/graphhopper-georouter.ts +++ b/src/modules/geography/infrastructure/graphhopper-georouter.ts @@ -164,12 +164,14 @@ export class GraphhopperGeorouter implements GeorouterPort { points: [[number, number]], snappedWaypoints: [[number, number]], ): number[] => { - const indices = snappedWaypoints.map((waypoint) => - points.findIndex( - (point) => point[0] == waypoint[0] && point[1] == waypoint[1], - ), + const indices: number[] = snappedWaypoints.map( + (waypoint: [number, number]) => + points.findIndex( + (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 .map( (value, index) =>