@tmlmobilidade/export-data 20260304.1819.11 → 20260421.1523.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +6 -0
- package/dist/prompts/export-types.js +3 -0
- package/dist/prompts/filter-line-ids.js +7 -13
- package/dist/prompts/filter-stop-ids.js +6 -13
- package/dist/tasks/executive-summary-setup/avg-trips-day.js +58 -0
- package/dist/tasks/executive-summary-setup/empty-runs.js +100 -0
- package/dist/tasks/executive-summary-setup/index.js +248 -0
- package/dist/tasks/executive-summary-setup/km.js +50 -0
- package/dist/tasks/executive-summary-setup/median-speed.js +80 -0
- package/dist/tasks/executive-summary-setup/on-board-sales.js +58 -0
- package/dist/tasks/executive-summary-setup/passenger-impact.js +46 -0
- package/dist/tasks/executive-summary-setup/passengers-transported.js +56 -0
- package/dist/tasks/executive-summary-setup/paxperkm.js +73 -0
- package/dist/tasks/executive-summary-setup/trips.js +132 -0
- package/dist/tasks/executive-summary-setup/tripstatus.js +114 -0
- package/dist/types.js +1 -0
- package/dist/utils/dates-helper.js +3 -0
- package/dist/utils/parse-id-list.js +37 -0
- package/package.json +4 -3
- package/dist/index.d.ts +0 -2
- package/dist/prompts/access-key.d.ts +0 -1
- package/dist/prompts/export-types.d.ts +0 -2
- package/dist/prompts/filter-agency-ids.d.ts +0 -1
- package/dist/prompts/filter-dates.d.ts +0 -5
- package/dist/prompts/filter-line-ids.d.ts +0 -1
- package/dist/prompts/filter-pattern-ids.d.ts +0 -1
- package/dist/prompts/filter-stop-ids.d.ts +0 -1
- package/dist/prompts/filter-types.d.ts +0 -1
- package/dist/prompts/filter-vehicle-ids.d.ts +0 -1
- package/dist/prompts/hashedshape-ids.d.ts +0 -1
- package/dist/prompts/validation-group-fields.d.ts +0 -2
- package/dist/tasks/apex-validations/validations-aggregated.d.ts +0 -18
- package/dist/tasks/apex-validations/validations-raw.d.ts +0 -2
- package/dist/tasks/hashed-shapes/hashed-shapes-geojson.d.ts +0 -4
- package/dist/tasks/rides/rides-raw.d.ts +0 -2
- package/dist/tasks/sams/sams-raw.d.ts +0 -2
- package/dist/tasks/sams/sams-raw.types.d.ts +0 -18
- package/dist/tasks/vehicle-events/vehicle-events-raw.d.ts +0 -2
- package/dist/types.d.ts +0 -35
- package/dist/utils/credential-storage.d.ts +0 -18
- package/dist/utils/hashed-shapes-to-geojson.d.ts +0 -3
- package/dist/utils/init-context.d.ts +0 -2
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Dates } from '@tmlmobilidade/dates';
|
|
2
|
+
import { simplifiedApexOnBoardSales } from '@tmlmobilidade/interfaces';
|
|
3
|
+
/* Main function */
|
|
4
|
+
export async function calculateOnBoardSales({ context, message }) {
|
|
5
|
+
message('Calculating on-board sales...');
|
|
6
|
+
const collection = await simplifiedApexOnBoardSales.getCollection();
|
|
7
|
+
// Convert received dates (yyyymmdd) to UTC range
|
|
8
|
+
const start = context.dates.start;
|
|
9
|
+
const end = context.dates.end;
|
|
10
|
+
const startDate = Dates.fromOperationalDate(start, 'Europe/Lisbon').unix_timestamp;
|
|
11
|
+
const endDate = Dates.fromOperationalDate(end, 'Europe/Lisbon').unix_timestamp;
|
|
12
|
+
// MongoDB pipeline based on your query
|
|
13
|
+
const pipeline = [
|
|
14
|
+
{
|
|
15
|
+
$match: {
|
|
16
|
+
agency_id: { $exists: true },
|
|
17
|
+
created_at: { $gte: startDate, $lte: endDate },
|
|
18
|
+
is_passenger: true,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
$group: {
|
|
23
|
+
_id: '$agency_id',
|
|
24
|
+
totalRevenueCents: { $sum: '$price' },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
$project: {
|
|
29
|
+
_id: 0,
|
|
30
|
+
agencyId: '$_id',
|
|
31
|
+
totalRevenue: {
|
|
32
|
+
$divide: ['$totalRevenueCents', 100], // convert to euros
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
$sort: { agencyId: 1 },
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
const cursor = collection.aggregate(pipeline);
|
|
41
|
+
const results = [];
|
|
42
|
+
// console.log('Pipeline results:', JSON.stringify(pipeline, null, 2));
|
|
43
|
+
for await (const doc of cursor) {
|
|
44
|
+
// Since the original query has no date, use startDate as reference
|
|
45
|
+
const date = new Date(startDate);
|
|
46
|
+
const day = date.getUTCDate().toString().padStart(2, '0');
|
|
47
|
+
const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
|
|
48
|
+
const year = date.getUTCFullYear();
|
|
49
|
+
const formattedDate = `${day}/${month}/${year}`;
|
|
50
|
+
results.push({
|
|
51
|
+
'agencyId': doc.agencyId,
|
|
52
|
+
'date': formattedDate,
|
|
53
|
+
'onboard-sales-pr': parseFloat(doc.totalRevenue.toFixed(2)),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
message(`Processed ${results.length} on-board sales records`);
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { yyyymmddToDashed } from '../../utils/dates-helper.js';
|
|
2
|
+
import { metrics } from '@tmlmobilidade/interfaces';
|
|
3
|
+
/**
|
|
4
|
+
* Calculates the estimated affected passengers by agency and day
|
|
5
|
+
* from the "demand_affected_by_failed_circulations_by_day" metric.
|
|
6
|
+
*/
|
|
7
|
+
export async function calculateAffectedPassengers({ context, message }) {
|
|
8
|
+
message('Calculating passengers affected by failed circulations...');
|
|
9
|
+
const metricsCollection = await metrics.getCollection();
|
|
10
|
+
// Fetch only the relevant metric documents
|
|
11
|
+
message('Fetching affected passengers metrics...');
|
|
12
|
+
const docs = await metricsCollection
|
|
13
|
+
.find({
|
|
14
|
+
metric: 'demand_affected_by_failed_circulations_by_day',
|
|
15
|
+
})
|
|
16
|
+
.toArray();
|
|
17
|
+
message(`Found ${docs.length} documents to process`);
|
|
18
|
+
const results = new Map(); // key = `${date}-${agencyId}`
|
|
19
|
+
const startDate = yyyymmddToDashed(context.dates.start);
|
|
20
|
+
const endDate = yyyymmddToDashed(context.dates.end);
|
|
21
|
+
// Process each metric document
|
|
22
|
+
for (const doc of docs) {
|
|
23
|
+
const agencyId = doc.properties.agency_id;
|
|
24
|
+
for (const [date, dayData] of Object.entries(doc.data)) {
|
|
25
|
+
// Skip dates outside the context range
|
|
26
|
+
if (date < startDate.replace(/-/g, '') || date > endDate.replace(/-/g, ''))
|
|
27
|
+
continue;
|
|
28
|
+
const dashedDate = yyyymmddToDashed(date);
|
|
29
|
+
const key = `${dashedDate}-${agencyId}`;
|
|
30
|
+
// Initialize entry if it doesn't exist
|
|
31
|
+
if (!results.has(key)) {
|
|
32
|
+
results.set(key, {
|
|
33
|
+
agencyId,
|
|
34
|
+
date: dashedDate,
|
|
35
|
+
estimatedpax_affected_by_failures: 0,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// Add estimated affected passengers
|
|
39
|
+
const entry = results.get(key);
|
|
40
|
+
entry.estimatedpax_affected_by_failures += dayData.estimated_affected_passengers;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
message(`Processed ${results.size} affected passenger records`);
|
|
44
|
+
// Return results as an array
|
|
45
|
+
return Array.from(results.values());
|
|
46
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { yyyymmddToDashed } from '../../utils/dates-helper.js';
|
|
2
|
+
import { metrics } from '@tmlmobilidade/interfaces';
|
|
3
|
+
/* * */
|
|
4
|
+
export async function calculateDemandFromMetrics({ context, message }) {
|
|
5
|
+
message('A calcular procura a partir das métricas...');
|
|
6
|
+
const metricsCollection = await metrics.getCollection();
|
|
7
|
+
//
|
|
8
|
+
// Fetch only relevant metrics
|
|
9
|
+
message('A ir buscar métricas de procura...');
|
|
10
|
+
const docs = await metricsCollection.find({
|
|
11
|
+
metric: { $in: ['demand_by_product_by_agency_by_day', 'demand_by_category_by_agency_by_day'] },
|
|
12
|
+
}).toArray();
|
|
13
|
+
message(`Encontradas ${docs.length} métricas para processar`);
|
|
14
|
+
const results = new Map(); // key = `${date}-${agencyId}`
|
|
15
|
+
const startDate = yyyymmddToDashed(context.dates.start);
|
|
16
|
+
const endDate = yyyymmddToDashed(context.dates.end);
|
|
17
|
+
//
|
|
18
|
+
// Process each metric document
|
|
19
|
+
for (const doc of docs) {
|
|
20
|
+
const agencyId = doc.properties.agency_id;
|
|
21
|
+
const isProductMetric = doc.metric === 'demand_by_product_by_agency_by_day';
|
|
22
|
+
for (const [date, dayData] of Object.entries(doc.data)) {
|
|
23
|
+
if (date < startDate || date > endDate)
|
|
24
|
+
continue;
|
|
25
|
+
const key = `${date}-${agencyId}`;
|
|
26
|
+
if (!results.has(key)) {
|
|
27
|
+
results.set(key, {
|
|
28
|
+
agencyId,
|
|
29
|
+
byCategory: {},
|
|
30
|
+
byProduct: {},
|
|
31
|
+
date,
|
|
32
|
+
totalpassengers: 0,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const entry = results.get(key);
|
|
36
|
+
const qty = dayData.qty;
|
|
37
|
+
if (isProductMetric) {
|
|
38
|
+
const productId = doc.properties.product_id;
|
|
39
|
+
entry.byProduct[productId] = (entry.byProduct[productId] || 0) + qty;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const category = doc.properties.category;
|
|
43
|
+
entry.byCategory[category] = (entry.byCategory[category] || 0) + qty;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//
|
|
48
|
+
// Calculate totalpassengers from byProduct
|
|
49
|
+
message('A calcular totais de passageiros...');
|
|
50
|
+
for (const entry of results.values()) {
|
|
51
|
+
entry.totalpassengers = Object.values(entry.byProduct).reduce((sum, v) => sum + v, 0);
|
|
52
|
+
}
|
|
53
|
+
message(`Processados ${results.size} registos de procura`);
|
|
54
|
+
return Array.from(results.values());
|
|
55
|
+
//
|
|
56
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Dates } from '@tmlmobilidade/dates';
|
|
2
|
+
import { rides } from '@tmlmobilidade/interfaces';
|
|
3
|
+
export async function calculatePassengersPerKm({ context }) {
|
|
4
|
+
console.log('Calculating passengers per km for executed trips...');
|
|
5
|
+
const ridesCollection = await rides.getCollection();
|
|
6
|
+
const startDateStr = Dates.fromOperationalDate(context.dates.start, 'Europe/Lisbon').unix_timestamp;
|
|
7
|
+
const endDateStr = Dates.fromOperationalDate(context.dates.end, 'Europe/Lisbon').unix_timestamp;
|
|
8
|
+
console.log(`Date range: ${startDateStr} to ${endDateStr}`);
|
|
9
|
+
const pipeline = [
|
|
10
|
+
{
|
|
11
|
+
$match: {
|
|
12
|
+
agency_id: { $exists: true },
|
|
13
|
+
start_time_scheduled: { $gte: startDateStr, $lte: endDateStr },
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
$project: {
|
|
18
|
+
agencyId: '$agency_id',
|
|
19
|
+
extensionScheduled: '$extension_scheduled',
|
|
20
|
+
isValid: {
|
|
21
|
+
$or: [
|
|
22
|
+
{ $eq: ['$analysis.SIMPLE_ONE_APEX_VALIDATION.grade', 'pass'] },
|
|
23
|
+
{ $eq: ['$analysis.SIMPLE_THREE_VEHICLE_EVENTS.grade', 'pass'] },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
passengersObserved: '$passengers_observed',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
$group: {
|
|
31
|
+
_id: { agencyId: '$agencyId' },
|
|
32
|
+
totalKm: { $sum: { $cond: ['$isValid', '$extensionScheduled', 0] } },
|
|
33
|
+
totalPassengers: { $sum: { $cond: ['$isValid', '$passengersObserved', 0] } },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
$project: {
|
|
38
|
+
_id: 0,
|
|
39
|
+
agencyId: '$_id.agencyId',
|
|
40
|
+
passengersPerKm: {
|
|
41
|
+
$cond: [
|
|
42
|
+
{ $eq: ['$totalKm', 0] },
|
|
43
|
+
0,
|
|
44
|
+
{
|
|
45
|
+
$round: [
|
|
46
|
+
{ $divide: ['$totalPassengers', { $divide: ['$totalKm', 1000] }] },
|
|
47
|
+
2,
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
totalKm: 1,
|
|
53
|
+
totalPassengers: 1,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
const aggCursor = ridesCollection.aggregate(pipeline);
|
|
58
|
+
const results = [];
|
|
59
|
+
for await (const doc of aggCursor) {
|
|
60
|
+
// // Total Passengers, Km, passengersPerKm DEBUG
|
|
61
|
+
// console.log(
|
|
62
|
+
// `[DEBUG] Agency: ${doc.agencyId} | Total Passengers: ${doc.totalPassengers} | Total Km: ${doc.totalKm} | passengersPerKm: ${doc.passengersPerKm}`,
|
|
63
|
+
// );
|
|
64
|
+
results.push({
|
|
65
|
+
agencyId: doc.agencyId,
|
|
66
|
+
passengersPerKm: doc.passengersPerKm,
|
|
67
|
+
totalKm: doc.totalKm,
|
|
68
|
+
totalPassengers: doc.totalPassengers,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
console.log(`Processed ${results.length} agencies for the interval.`);
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Dates } from '@tmlmobilidade/dates';
|
|
2
|
+
import { rides } from '@tmlmobilidade/interfaces';
|
|
3
|
+
/* * */
|
|
4
|
+
/**
|
|
5
|
+
* Calculates the number of planned trips per agency and date
|
|
6
|
+
*/
|
|
7
|
+
export async function calculatePlannedTrips({ context, message }) {
|
|
8
|
+
message('Calculating planned trips...');
|
|
9
|
+
const ridesCollection = await rides.getCollection();
|
|
10
|
+
const startDateStr = Dates.fromOperationalDate(context.dates.start, 'Europe/Lisbon').unix_timestamp;
|
|
11
|
+
const endDateStr = Dates.fromOperationalDate(context.dates.end, 'Europe/Lisbon').unix_timestamp;
|
|
12
|
+
message(`Date range: ${startDateStr} to ${endDateStr}`);
|
|
13
|
+
const pipeline = [
|
|
14
|
+
{
|
|
15
|
+
$match: {
|
|
16
|
+
agency_id: { $exists: true },
|
|
17
|
+
start_time_scheduled: { $gte: startDateStr, $lte: endDateStr },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
$group: {
|
|
22
|
+
_id: { agencyId: '$agency_id', date: '$operational_date' },
|
|
23
|
+
plannedTrips: { $sum: 1 },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
$project: {
|
|
28
|
+
_id: 0,
|
|
29
|
+
agencyId: '$_id.agencyId',
|
|
30
|
+
date: '$_id.date',
|
|
31
|
+
plannedTrips: 1,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
const aggCursor = ridesCollection.aggregate(pipeline);
|
|
36
|
+
const results = [];
|
|
37
|
+
for await (const doc of aggCursor) {
|
|
38
|
+
const formattedDate = Dates.fromOperationalDate(doc.date, 'Europe/Lisbon').toFormat('dd/MM/yyyy');
|
|
39
|
+
results.push({
|
|
40
|
+
agencyId: doc.agencyId,
|
|
41
|
+
date: formattedDate,
|
|
42
|
+
plannedTrips: doc.plannedTrips,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
message(`Processed ${results.length} planned trips metrics`);
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
48
|
+
/* * */
|
|
49
|
+
/**
|
|
50
|
+
* Calculates the number of completed trips per agency and date
|
|
51
|
+
*/
|
|
52
|
+
export async function calculateCompletedTrips({ context, message }) {
|
|
53
|
+
message('Calculating completed trips...');
|
|
54
|
+
const ridesCollection = await rides.getCollection();
|
|
55
|
+
const startDateStr = Dates.fromOperationalDate(context.dates.start, 'Europe/Lisbon').toFormat('yyyyMMdd');
|
|
56
|
+
const endDateStr = Dates.fromOperationalDate(context.dates.end, 'Europe/Lisbon').toFormat('yyyyMMdd');
|
|
57
|
+
message(`Date range: ${startDateStr} to ${endDateStr}`);
|
|
58
|
+
const pipeline = [
|
|
59
|
+
{
|
|
60
|
+
$match: {
|
|
61
|
+
agency_id: { $exists: true },
|
|
62
|
+
operational_date: { $gte: startDateStr, $lte: endDateStr },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
$group: {
|
|
67
|
+
_id: { agencyId: '$agency_id', date: '$operational_date' },
|
|
68
|
+
completedTrips: {
|
|
69
|
+
$sum: {
|
|
70
|
+
$cond: [
|
|
71
|
+
{
|
|
72
|
+
$or: [
|
|
73
|
+
{ $eq: ['$analysis.SIMPLE_ONE_APEX_VALIDATION.grade', 'pass'] },
|
|
74
|
+
{ $eq: ['$analysis.SIMPLE_THREE_VEHICLE_EVENTS.grade', 'pass'] },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
1,
|
|
78
|
+
0,
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
$project: {
|
|
86
|
+
_id: 0,
|
|
87
|
+
agencyId: '$_id.agencyId',
|
|
88
|
+
completedTrips: 1,
|
|
89
|
+
date: '$_id.date',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
const aggCursor = ridesCollection.aggregate(pipeline);
|
|
94
|
+
const results = [];
|
|
95
|
+
for await (const doc of aggCursor) {
|
|
96
|
+
const formattedDate = Dates.fromOperationalDate(doc.date, 'Europe/Lisbon').toFormat('dd/MM/yyyy');
|
|
97
|
+
results.push({
|
|
98
|
+
agencyId: doc.agencyId,
|
|
99
|
+
completedTrips: doc.completedTrips,
|
|
100
|
+
date: formattedDate,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
message(`Processed ${results.length} completed trips metrics`);
|
|
104
|
+
return results;
|
|
105
|
+
}
|
|
106
|
+
/* * */
|
|
107
|
+
/**
|
|
108
|
+
* Calculates the percentage of completed trips based on planned trips
|
|
109
|
+
*/
|
|
110
|
+
export function calculateCompletedTripsPercentage(plannedTrips, completedTrips) {
|
|
111
|
+
const percentageResults = [];
|
|
112
|
+
// Create a lookup map for planned trips
|
|
113
|
+
const plannedMap = new Map();
|
|
114
|
+
for (const p of plannedTrips) {
|
|
115
|
+
plannedMap.set(`${p.date}-${p.agencyId}`, p.plannedTrips);
|
|
116
|
+
}
|
|
117
|
+
// Calculate % for each completed trip entry
|
|
118
|
+
for (const c of completedTrips) {
|
|
119
|
+
const key = `${c.date}-${c.agencyId}`;
|
|
120
|
+
const planned = plannedMap.get(key) || 0;
|
|
121
|
+
let percentage = 0;
|
|
122
|
+
if (planned > 0) {
|
|
123
|
+
percentage = (c.completedTrips / planned) * 100;
|
|
124
|
+
}
|
|
125
|
+
percentageResults.push({
|
|
126
|
+
agencyId: c.agencyId,
|
|
127
|
+
date: c.date,
|
|
128
|
+
percentage: parseFloat(percentage.toFixed(1)), // round to 1 decimal
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return percentageResults;
|
|
132
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Dates } from '@tmlmobilidade/dates';
|
|
2
|
+
import { rides } from '@tmlmobilidade/interfaces';
|
|
3
|
+
import { Logger } from '@tmlmobilidade/logger';
|
|
4
|
+
import { Timer } from '@tmlmobilidade/timer';
|
|
5
|
+
/* Helper Function */
|
|
6
|
+
async function processDailyRides(stream, results, agencies, agency43Trips) {
|
|
7
|
+
for (const rideData of stream) {
|
|
8
|
+
const agency = rideData.agency_id;
|
|
9
|
+
if (!agencies.includes(agency))
|
|
10
|
+
continue;
|
|
11
|
+
if (rideData.system_status !== 'complete')
|
|
12
|
+
continue;
|
|
13
|
+
const analysis = rideData.analysis;
|
|
14
|
+
if (!analysis)
|
|
15
|
+
continue;
|
|
16
|
+
const expectedStart = analysis.EXPECTED_START_TIME?.value;
|
|
17
|
+
if (expectedStart === undefined || expectedStart === null)
|
|
18
|
+
continue;
|
|
19
|
+
if (expectedStart <= -1) {
|
|
20
|
+
results.agencies[agency].early_rides++;
|
|
21
|
+
results.total.early_rides++;
|
|
22
|
+
}
|
|
23
|
+
if (expectedStart >= 5) {
|
|
24
|
+
results.agencies[agency].five_min_delays++;
|
|
25
|
+
results.total.five_min_delays++;
|
|
26
|
+
const passengers = rideData.passengers_observed ?? 0;
|
|
27
|
+
results.agencies[agency]['pax-affected-by-delay']
|
|
28
|
+
= (results.agencies[agency]['pax-affected-by-delay'] ?? 0) + passengers;
|
|
29
|
+
results.total['pax-affected-by-delay']
|
|
30
|
+
= (results.total['pax-affected-by-delay'] ?? 0) + passengers;
|
|
31
|
+
if (agency === '43') {
|
|
32
|
+
agency43Trips.push(rideData.trip_id);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (expectedStart > -1 && expectedStart < 5) {
|
|
36
|
+
results.agencies[agency].ontime_rides++;
|
|
37
|
+
results.total.ontime_rides++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/* Percentages */
|
|
42
|
+
function calculatePercentages(metrics) {
|
|
43
|
+
const total = metrics.early_rides + metrics.five_min_delays + metrics.ontime_rides;
|
|
44
|
+
if (total === 0)
|
|
45
|
+
return { early_rides_pct: 0, five_min_delays_pct: 0, ontime_rides_pct: 0 };
|
|
46
|
+
return {
|
|
47
|
+
early_rides_pct: parseFloat(((metrics.early_rides / total) * 100).toFixed(1)),
|
|
48
|
+
five_min_delays_pct: parseFloat(((metrics.five_min_delays / total) * 100).toFixed(1)),
|
|
49
|
+
ontime_rides_pct: parseFloat(((metrics.ontime_rides / total) * 100).toFixed(1)),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/* Main Function */
|
|
53
|
+
export const calculateDailyServiceCompliance = async (context) => {
|
|
54
|
+
const agencies = ['41', '42', '43', '44'];
|
|
55
|
+
const ridesCollection = await rides.getCollection();
|
|
56
|
+
const agency43Trips = [];
|
|
57
|
+
const processingTimer = new Timer();
|
|
58
|
+
let countProcessed = 0;
|
|
59
|
+
const resultsByDay = {};
|
|
60
|
+
const startDate = Dates.fromOperationalDate(context.dates.start, 'Europe/Lisbon').unix_timestamp;
|
|
61
|
+
const endDate = Dates.fromOperationalDate(context.dates.end, 'Europe/Lisbon').unix_timestamp;
|
|
62
|
+
const ridesQuery = {
|
|
63
|
+
start_time_scheduled: {
|
|
64
|
+
$gte: startDate,
|
|
65
|
+
$lte: endDate,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
const allRidesStream = ridesCollection.find(ridesQuery).batchSize(100_000).stream();
|
|
69
|
+
for await (const ride of allRidesStream) {
|
|
70
|
+
const rideData = ride;
|
|
71
|
+
const dayOperationalStr = Dates.fromOperationalDate(rideData.operational_date, 'Europe/Lisbon').toFormat('yyyyMMdd');
|
|
72
|
+
if (!resultsByDay[dayOperationalStr]) {
|
|
73
|
+
resultsByDay[dayOperationalStr] = {
|
|
74
|
+
agencies: {},
|
|
75
|
+
total: { 'early_rides': 0, 'five_min_delays': 0, 'ontime_rides': 0, 'pax-affected-by-delay': 0 },
|
|
76
|
+
};
|
|
77
|
+
agencies.forEach((op) => {
|
|
78
|
+
resultsByDay[dayOperationalStr].agencies[op] = {
|
|
79
|
+
'early_rides': 0,
|
|
80
|
+
'five_min_delays': 0,
|
|
81
|
+
'ontime_rides': 0,
|
|
82
|
+
'pax-affected-by-delay': 0,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
await processDailyRides([rideData], resultsByDay[dayOperationalStr], agencies, agency43Trips);
|
|
87
|
+
countProcessed++;
|
|
88
|
+
if (countProcessed % 100 === 0) {
|
|
89
|
+
// Logger.info(`Processed ${countProcessed} rides so far...`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
Logger.info(`Ride processing completed in ${processingTimer.get()} ms`);
|
|
93
|
+
/* Format results and calculate percentages */
|
|
94
|
+
const formattedResults = {};
|
|
95
|
+
for (const day in resultsByDay) {
|
|
96
|
+
const displayDate = Dates.fromOperationalDate(day, 'Europe/Lisbon').toFormat('dd/MM/yyyy');
|
|
97
|
+
const dayMetrics = resultsByDay[day];
|
|
98
|
+
const agenciesWithPct = {};
|
|
99
|
+
for (const agencyId of Object.keys(dayMetrics.agencies)) {
|
|
100
|
+
const percentages = calculatePercentages(dayMetrics.agencies[agencyId]);
|
|
101
|
+
agenciesWithPct[agencyId] = { ...dayMetrics.agencies[agencyId], ...percentages };
|
|
102
|
+
}
|
|
103
|
+
const totalPercentages = calculatePercentages(dayMetrics.total);
|
|
104
|
+
const totalWithPct = { ...dayMetrics.total, ...totalPercentages };
|
|
105
|
+
formattedResults[displayDate] = {
|
|
106
|
+
agencies: agenciesWithPct,
|
|
107
|
+
total: totalWithPct,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/* LOG: Export agency 43 trips */
|
|
111
|
+
// fs.writeFileSync('agency_43_trips.json', JSON.stringify(agency43Trips, null, 2));
|
|
112
|
+
// Logger.info(`Exported ${agency43Trips.length} trip_id for agency 43 to agency_43_trips.json`);
|
|
113
|
+
return formattedResults;
|
|
114
|
+
};
|
package/dist/types.js
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
// Parses "id1,id2,start-end" into a flat list of IDs; expands ranges (e.g. 1001-1003)
|
|
3
|
+
export function parseIdListWithRanges(input, digits) {
|
|
4
|
+
const re = new RegExp(`^(\\d{${digits}})-(\\d{${digits}})$`);
|
|
5
|
+
const parts = input.replace(/,$/, '').split(',').map(id => id.trim());
|
|
6
|
+
const result = [];
|
|
7
|
+
for (const part of parts) {
|
|
8
|
+
const m = part.match(re);
|
|
9
|
+
if (m) {
|
|
10
|
+
const lo = Number(m[1]);
|
|
11
|
+
const hi = Number(m[2]);
|
|
12
|
+
for (let n = lo; n <= hi; n++)
|
|
13
|
+
result.push(String(n).padStart(digits, '0'));
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
result.push(part);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
// Returns an error message if the value is invalid, otherwise undefined
|
|
22
|
+
export function validateIdListWithRanges(value, digits) {
|
|
23
|
+
const re = new RegExp(`^(\\d{${digits}})-(\\d{${digits}})$`);
|
|
24
|
+
const singleRe = new RegExp(`^\\d{${digits}}$`);
|
|
25
|
+
const parts = value.replace(/,$/, '').split(',').map(id => id.trim());
|
|
26
|
+
for (const part of parts) {
|
|
27
|
+
const m = part.match(re);
|
|
28
|
+
if (m) {
|
|
29
|
+
if (Number(m[1]) > Number(m[2]))
|
|
30
|
+
return `Intervalo inválido: ${part}`;
|
|
31
|
+
}
|
|
32
|
+
else if (!singleRe.test(part)) {
|
|
33
|
+
return `ID ou intervalo inválido: ${part}`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tmlmobilidade/export-data",
|
|
3
3
|
"description": "CLI tool to export data from GO.",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "20260421.1523.13",
|
|
5
5
|
"author": {
|
|
6
6
|
"email": "iso@tmlmobilidade.pt",
|
|
7
7
|
"name": "TML-ISO"
|
|
@@ -36,9 +36,10 @@
|
|
|
36
36
|
"lint:fix": "eslint . --fix"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@clack/prompts": "1.0
|
|
39
|
+
"@clack/prompts": "1.2.0",
|
|
40
40
|
"@tmlmobilidade/consts": "*",
|
|
41
41
|
"@tmlmobilidade/dates": "*",
|
|
42
|
+
"@tmlmobilidade/go-performance-pckg-dates": "*",
|
|
42
43
|
"@tmlmobilidade/interfaces": "*",
|
|
43
44
|
"@tmlmobilidade/strings": "*",
|
|
44
45
|
"@tmlmobilidade/timer": "*",
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@tmlmobilidade/tsconfig": "*",
|
|
49
50
|
"@tmlmobilidade/types": "*",
|
|
50
|
-
"@types/node": "25.
|
|
51
|
+
"@types/node": "25.6.0",
|
|
51
52
|
"resolve-tspaths": "0.8.23",
|
|
52
53
|
"typescript": "5.9.3"
|
|
53
54
|
}
|
package/dist/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function promptAccessKey(): Promise<void>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function promptFilterByAgencyIds(): Promise<string[]>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function promptFilterByLineIds(): Promise<string[]>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function promptFilterByPatternIds(): Promise<string[]>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function promptFilterByStopIds(): Promise<string[]>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function promptFilterTypes(): Promise<string[]>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function promptFilterByVehicleIds(): Promise<number[]>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function promptHashedShapeIds(): Promise<string[]>;
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { type TaskProps } from '../../types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Available fields for grouping validations.
|
|
4
|
-
* 'date' is always included by default.
|
|
5
|
-
*/
|
|
6
|
-
export declare const validationGroupFields: readonly ["line_id", "pattern_id", "product_id", "trip_id", "stop_id", "agency_id", "vehicle_id"];
|
|
7
|
-
export type ValidationGroupField = typeof validationGroupFields[number];
|
|
8
|
-
export declare const validationGroupFieldLabels: Record<ValidationGroupField, string>;
|
|
9
|
-
interface ValidationAggregatedTaskProps extends TaskProps {
|
|
10
|
-
groupFields: ValidationGroupField[];
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Export Validations aggregated by user-selected fields.
|
|
14
|
-
* Always groups by date, plus any additional fields selected by the user.
|
|
15
|
-
* The 'validations' count is always included.
|
|
16
|
-
*/
|
|
17
|
-
export declare function exportValidationsAggregated({ context, groupFields, message }: ValidationAggregatedTaskProps): Promise<void>;
|
|
18
|
-
export {};
|