Created
January 2, 2026 21:00
-
-
Save tatethurston/87434fef1f614b38e1b0c4a6ad61edf8 to your computer and use it in GitHub Desktop.
Lyft Bike Monthly Receipt Aggregator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class Month { | |
| static previous(date) { | |
| return this.month(date, { offset: -1 }); | |
| } | |
| static current(date) { | |
| return this.month(date, { offset: 0 }); | |
| } | |
| static next(date) { | |
| return this.month(date, { offset: 1 }); | |
| } | |
| static month(date, { offset }) { | |
| return new Date( | |
| date.getFullYear(), | |
| date.getMonth() + offset, | |
| 1, | |
| 0, 0, 0, 0 | |
| ) | |
| } | |
| } | |
| class LyftQuery { | |
| constructor({ after, before }) { | |
| this.query = [ | |
| 'from:no-reply@lyftmail.com', | |
| 'subject:receipt', | |
| `after:${this.format(after)}`, | |
| `before:${this.format(before)}` | |
| ].join(' '); | |
| } | |
| format(date) { | |
| return Utilities.formatDate(date, 'PST', 'yyyy/MM/dd'); | |
| } | |
| search() { | |
| Logger.log(`Searching "${this.query}"`); | |
| const threads = GmailApp.search(this.query); | |
| return threads; | |
| } | |
| } | |
| class LyftEmailParser { | |
| static normalize(html) { | |
| return html | |
| .replace(/=\r?\n/g, '') | |
| .replace(/<style[\s\S]*?<\/style>/gi, '') | |
| .replace(/<script[\s\S]*?<\/script>/gi, '') | |
| .replace(/\s+/g, ' '); | |
| } | |
| static extractBikeRides(message) { | |
| const html = this.normalize(message.getBody()) | |
| const rides = []; | |
| const rideRegex = | |
| /(January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2},\s+\d{4}\s+\d{1,2}:\d{2}\s+(AM|PM)[\s\S]*?\$([\d.]+)[\s\S]*?Bike\/scooter fare/gi; | |
| let match; | |
| while ((match = rideRegex.exec(html)) !== null) { | |
| rides.push({ | |
| amount: Number(match[3]), | |
| date: match[0].match( | |
| /(January|February|March|April|May|June|July|August|September|October|November|December)[^<]+/ | |
| )[0], | |
| }); | |
| } | |
| return { | |
| message, | |
| rides | |
| }; | |
| } | |
| static isWeekend(ride) { | |
| const day = new Date (ride.rides[0].date); | |
| return [0,6].includes(day.getDay()); | |
| } | |
| static parse(threads) { | |
| const total = threads | |
| .flatMap(thread => thread.getMessages()) | |
| .map(message => this.extractBikeRides(message)) | |
| const rides = total.filter(ride => { | |
| if (this.isWeekend(ride)) { | |
| Logger.log(`Omitting ride because it fell on a weekend: ${JSON.stringify(ride)}`); | |
| return false; | |
| } | |
| return true; | |
| }); | |
| const validRides = rides.reduce((acc, cur) => acc + cur.rides.length, 0); | |
| const totalRides = total.reduce((acc, cur) => acc + cur.rides.length, 0); | |
| Logger.log(`Using ${validRides} / ${totalRides} rides`); | |
| return rides; | |
| } | |
| static summary(messages) { | |
| return messages.flatMap(message => message.rides) | |
| .reduce((acc, cur) => acc + cur.amount, 0) | |
| } | |
| static consolidated(messages, date) { | |
| const content = messages.sort((a, b) => b.message.getDate() - a.message.getDate()); | |
| const month = Utilities.formatDate(date, 'PST', 'MMM YYYY'); | |
| const html = [ | |
| `<body> | |
| <h1>Consolidated Statement</h1> | |
| <h2>${month}</h2> | |
| <h2>Total: ${this.summary(messages)}</h2> | |
| </body>`, | |
| ...content.map(message => message.message.getBody()) | |
| ].join(''); | |
| return html; | |
| } | |
| } | |
| class EmailGateway { | |
| static send(html) { | |
| const blob = Utilities | |
| .newBlob(html, 'text/html') | |
| .getAs('application/pdf') | |
| .setName(`Lyft_Bike_Receipts.pdf`); | |
| MailApp.sendEmail({ | |
| to: Session.getActiveUser().getEmail(), | |
| subject: 'Lyft Bike Receipts', | |
| body: Logger.getLog(), | |
| attachments: [blob] | |
| }); | |
| } | |
| } | |
| function main() { | |
| const today = new Date('12/01/2025'); | |
| const query = new LyftQuery({ | |
| after: Month.previous(today), | |
| before: Month.current(today), | |
| }); | |
| threads = query.search(); | |
| Logger.log(`Found ${threads.length} emails`); | |
| const messages = LyftEmailParser.parse(threads); | |
| const html = LyftEmailParser.consolidated(messages, Month.previous(today)); | |
| EmailGateway.send(html) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment