BikeTripDurations.ts 4.21 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import StatusCodes from 'http-status-codes'
import { Request, Response, Router } from 'express'
import { ApiError, paramMissingError } from '@shared/responseTypes'
import { IBikeTripDurationCount } from '@entities/BikeTripDurationCount'
import { bikeTripsCollectionName, dbClient, dbName } from '@server'

const router = Router()
const {OK} = StatusCodes

interface IBikeTripDurationsResponse {
    bikeTripDurationCounts: IBikeTripDurationCount[]
}

interface QueryParams {
    classSize: string
    from: string
    to: string
}

/***************************************************************************************
 *         Get Bike Trip Duration Statistics in range - "GET /api/bike-trip-durations"
 ***************************************************************************************/

router.get('/', async (req: Request<any, any, any, QueryParams>, res: Response<IBikeTripDurationsResponse | ApiError>) => {
    // class size in seconds
    const durationClassSize = Number(req.query.classSize)
    const from = Number(req.query.from)
    const to = Number(req.query.to)

    if (!durationClassSize || !from || !to) {
        return res.status(StatusCodes.BAD_REQUEST).json({error: paramMissingError})
    }

    // create classes between 0 and 90 minutes
    const maxInterestingDuration = 90 * 60
    const numberOfDurationClasses = Math.floor(maxInterestingDuration / durationClassSize)

    // efficient mongodb aggregation query, consists of multiple 'stages'
    // _id will be the max duration (exclusive) of the according class after the aggregation
    const durationClassCountsFromDB = await dbClient.db(dbName).collection(bikeTripsCollectionName).aggregate<{ _id: number, count: number }>([
        // only use data of correct time range
        {$match: {startDate: {$gte: from, $lt: to}}},
        {$match: {duration: {$gte: 0}}},
        // count for different trip duration intervals, how many trips fall into them
        // _id will container the upper limit of the respective duration interval
        {$group: {
            _id: {
                $switch: {
                    branches: range(durationClassSize, maxInterestingDuration, durationClassSize).map(maxDurationOfClass => ({
                        case: {$lt: ['$duration', maxDurationOfClass]},
                        then: maxDurationOfClass
                    })),
                    default: (numberOfDurationClasses + 1) * durationClassSize
                }
            },
            count: {$sum: 1}
        }},
        {$sort: {_id: 1}},
    ]).toArray()

    // MongoDB query result might not contain all duration classes, if there were no bike trips with that duration
    const necessaryClasses: { _id: number, count: number }[] = []

    // add values to all duration classes, if none set to 0
    for (let i = 0; i < numberOfDurationClasses + 1; i++) {
        const currentClassToCheck = (i + 1) * durationClassSize
        necessaryClasses[i] = {_id: currentClassToCheck, count: 0}

        for (let n = 0; n < durationClassCountsFromDB.length; n++) {
            if (durationClassCountsFromDB[n]._id == currentClassToCheck) {
                necessaryClasses[i].count = durationClassCountsFromDB[n].count
                break
            }
        }
    }

    // prepare output or diagram, with duration class labels
    const bikeTripDurationCounts = necessaryClasses.map((durationClassCount, index) => {
        const fromValue = durationClassCount._id - durationClassSize
        const toValue = durationClassCount._id

        let classLabel = `${fromValue / 60}-${toValue / 60}`

        if (index === necessaryClasses.length - 1) {
            classLabel = `${fromValue / 60}+`
        }

        return {
            classLabel: classLabel,
            count: durationClassCount.count
        } as IBikeTripDurationCount
    })

    return res.status(OK).json({bikeTripDurationCounts})
})

// Sequence generator function
const range = (start: number, stop: number, step: number) => Array.from({length: (stop - start) / step + 1}, (_, i) => start + (i * step))


/******************************************************************************
 *                                 Export
 ******************************************************************************/

export default router