Here’s a coding challenge that aligns with the description:
Challenge: Invoice Due Date Tracker
Scenario:
You are working on a tool for a small business that manages invoices. The business needs a simple program to track invoice due dates and alert the owner if any invoices are overdue.
Requirements:
-
Input:
- A list of invoices, where each invoice has:
id
(string): A unique identifier.amount
(number): The amount due.dueDate
(string): The due date inYYYY-MM-DD
format.
- A second input, the current date in
YYYY-MM-DD
format.
- A list of invoices, where each invoice has:
-
Output:
- A list of overdue invoices containing:
id
amount
daysOverdue
(number): The number of days past the due date.
- A list of overdue invoices containing:
-
Behavior:
- Parse the input data.
- Calculate how many days each overdue invoice is past its due date.
- Handle edge cases like invalid dates or missing fields gracefully by ignoring invalid entries and logging an error.
-
Stretch Features:
- Add a feature to group overdue invoices by how many days overdue they are (e.g.,
1-7 days
,8-30 days
,31+ days
).
- Add a feature to group overdue invoices by how many days overdue they are (e.g.,
Example Input:
typescript
Copy code
const invoices = [ { id: "INV-001", amount: 200.50, dueDate: "2024-12-01" }, { id: "INV-002", amount: 150.00, dueDate: "2024-11-20" }, { id: "INV-003", amount: 450.75, dueDate: "2024-12-10" }, { id: "INV-004", amount: 300.00, dueDate: "invalid-date" }, ]; const currentDate = "2024-12-07";
Example Output:
typescript
Copy code
[ { id: "INV-001", amount: 200.50, daysOverdue: 6 }, { id: "INV-002", amount: 150.00, daysOverdue: 17 }, ]
Tips:
- Use libraries like
date-fns
orluxon
for date parsing if you want, or handle dates manually using JavaScript’sDate
object. - Be prepared to discuss why you chose a particular approach (e.g., handling dates, iterating over data, logging errors).
- Test your solution on edge cases, like empty arrays or invoices with missing fields.
Would you like me to help flesh out any part of this further?
My Execution
The first issue I really ran into was that I was trying to use a map to iterate through the invoices that I parsed out using invoices.split('\n\n)
, but a map will always return a value I think. If you return nothing, it’s actually undefined.
So it’s not a good match for validation. Starting with a normal loop would be much better. I’m also finding that maps are a bit dangerous, because they can be a bit harder to reason about sometimes.
HOWEVER, you could do a filter operation that got rid of the undefined ones.
Runtime Validation
For something like an invoice checker, it might be really good to use a runtime validation library like Zod or Yup.
I’m not quite used to them enough but now that I think about it they are really, really helpful for something like this.regex
Attempt 3
I’m going to omit my first two attempts because I only gave myself twenty minutes to do them. This is what I ended up with when I gave myself about 50-60 minutes:
// divide invoices into separate ones
// divide them into four separate strings
// Will take the most time, error checking, etc.
// using a validaton library would really improve the safety and readability of this section
// process the strings into dates, numbers, and the difference between two dates
// ^ one function that does this, try this function with one result and see how it does
// parse the date
// do the date math
// Easy
// filter based on overdues, only and the overdue invoices to a list,
// include the amount and then process the overdue as Days Overude
// very easy to do in O(n), could improve with accumulator function
// use a hashmap to group overdues by days
// then sort in an array
// Requires some research
// provide a timezone to do these calculations in
// if they occur across timezone changes for say, daylight's savings, that could lead to very serious errors
import fs from 'fs'
interface UnparsedInvoice {
id: string,
amount: string,
dueDate: string,
currentDate: string
}
interface ParsedInvoice {
id: number,
amount: number,
dueDate: Date,
currentDate: Date
}
interface ProcessedInvoice {
id: number,
amount: number,
daysToOverdue: number
}
interface OverdueInvoice {
id: number,
amount: number,
overdueBy: number
}
const fileName = "invoices1.txt"
fs.readFile(fileName, "utf-8", (err, data)=>{
try{
console.assert(data && data.length > 4, "data in file is missing or shorter than expect. Do you have the write file?")
const sections: string[] = data.trim().split("\n\n")
const rightSizedSections: UnparsedInvoice[] = []
for (let i = 0; i < sections.length; i++){
const currentSection = sections[i]
const attemptAtStructuringData = processDataLines(currentSection)
if (attemptAtStructuringData){
rightSizedSections.push(attemptAtStructuringData)
}
}
const parsedInvoices: ParsedInvoice[] = []
for (let i = 0; i < rightSizedSections.length; i++){
const unparsedInvoice = rightSizedSections[i]
const parsedInvoiceAttempt = parseInvoice(unparsedInvoice)
if (parsedInvoiceAttempt) parsedInvoices.push(parsedInvoiceAttempt)
}
const porcessedInvoices = parsedInvoices.map(pi=>{
return processInvoice(pi)
}).filter(v=>v)
console.log(porcessedInvoices)
} catch (e){
console.error(`Issue reading file ${fileName}:`, e)
}
})
// Takes in a single invoice, and outputs an UnparsedInvoice object
function processDataLines(input: string): UnparsedInvoice | null {
const lines = input.split('\n').map(line=>{
return line.split(": ")[1]
})
if (lines.length !== 4){
console.error(`found too few lines for item with first entry of ${lines[0]}`)
return null
}
const [id, amount, dueDate, currentDate] = lines
return {id, amount, dueDate, currentDate}
}
function coerceToNumber(input: string): number | null {
const numberMatches = input.match(/\d+/g)
if (!numberMatches || !numberMatches.length){
return null
}
const parsedNumber = Number(numberMatches[0])
return parsedNumber
}
function coerceToFloat(input: string): number | null {
const numberMatches = input.match(/[\d.]+/g)
if (!numberMatches || !numberMatches.length){
return null
}
const parsedNumber = parseFloat(numberMatches[0])
return parsedNumber
}
function buildDateYYYYMMDD(input: string): Date | null {
const parts = input.split('-')
if (!parts || parts.length < 3){
return null
}
const parsedParts = parts.map(part=>{
return coerceToNumber(part)
})
const [year, month, day] = parsedParts
console.assert(year && month && day, "buildDateYYYYMMDD passed unparsable input")
if (month! > 12){
console.error(`month of ${month} was greater than 12, buildDateYYYYMMDD couldn't parse it`)
return null
}
if (day! > 31){
console.error(`day of ${day} was greater than 31, buildDateYYYYMMDD couldn't parse it`)
return null
}
if (year!.toString().length !== 4){
console.error(`buildDateYYYYMMDD requires a year of 4 digits: ${year} has ${year!.toString().length}`)
return null
}
const zeroIndexedMonth = month!-1
if (year && month && day){
return new Date(Date.UTC(year, zeroIndexedMonth, day))
}
return null
}
function parseInvoice(invoice: UnparsedInvoice): ParsedInvoice | null {
const parsedId = coerceToNumber(invoice.id)
const parsedAmount = coerceToFloat(invoice.amount)
const dueDate = buildDateYYYYMMDD(invoice.dueDate)
const currentDate = buildDateYYYYMMDD(invoice.currentDate)
if (!(parsedId && parsedAmount && currentDate && dueDate)) return null
// TODO: our amount is being rounded down
return {
id: parsedId,
amount: parsedAmount,
dueDate,
currentDate
}
}
function processInvoice(invoice: ParsedInvoice): OverdueInvoice | null {
const dayInMilliseconds = 1000 * 60 * 60 * 24
const daysOverdue = (invoice.currentDate.getTime() - invoice.dueDate.getTime()) / dayInMilliseconds
console.log(daysOverdue)
if (daysOverdue <= 0) return null
return {
id: invoice.id,
amount: invoice.amount,
overdueBy: daysOverdue
}
}
console.assert(coerceToFloat("123123.345345") === 123123.345345, "coerceToFloat didn't return as expected")
console.assert(coerceToFloat("123123.345345asdf") === 123123.345345, "coerceToFloat didn't return as expected")
console.assert(coerceToFloat("asdf123123.345345asdfasdf") === 123123.345345, "coerceToFloat didn't return as expected")
console.assert(coerceToFloat("asdf123123.345345asdfasdf123") === 123123.345345, "coerceToFloat didn't return as expected")
console.assert(coerceToNumber("234567")===234567, "coerce to number not returning as expected")
console.assert(coerceToNumber("23456asdf")===23456, "coerce to number not returning as expected")
console.assert(coerceToNumber("asdf234567")===234567, "coerce to number not returning as expected")
console.assert(coerceToNumber("asdf23asdf4567")===23, "coerce to number not returning as expected")
Thoughts
I feel good about this. Like, this makes me feel joy.
Things that went well:
- The write-as-I-go error handling I built in saved my ass a few times. It sort of felt like overwriting, but it was so needed as potential mistakes are quite plentiful
- On the same theme, I would read over a function after writing it to make sure that the data flow looked roughly safe, and correct. It almost always had some issues to be cleaned up, or at least suspicions to be covered with error handling. I think this was a timesaver over console.logging everything because it gave me coverage into the future that didn’t need to be done manually.
- Reading the docs - there was a moment when reading the native JS Date object docs reminded me that when passing months into new Date(), they are 0-indexed, a gotcha that could totally have tripped me up
- However, I also compared the input in detail to the output, which gave me lots of clues, like realizing that the input I wrote actually had a ton of invalid months, allowing me to write in error handling and invalidate all of my existing data and write more
Things that were kinda tricky:
- It didn’t go poorly, but it was just time consuming to do validation. I’d like to find faster ways to do it.
To Study
- Mostly Date
- When does date parse parse strings as UTC and when does it not?
Regex to parse date formats
const regex = /^\d{4}-\d{2}-\d{2}$/;
Borrowed from ChatGPT’s buildDateYYYYMMDD:
function buildDateYYYYMMDD(input: string): Date | null {
// Use a regex to validate the input format
const regex = /^\d{4}-\d{2}-\d{2}$/;
if (!regex.test(input)) {
console.error(`Invalid date format: ${input}`);
return null;
}
// Parse the date and ensure it's valid
const parsedDate = new Date(`${input}T00:00:00Z`); // Ensure UTC parsing
if (isNaN(parsedDate.getTime())) {
console.error(`Invalid date value: ${input}`);
return null;
}
return parsedDate;
}
I don’t know if I love this, but it’s okay. I do like the way it handle the date itself, but not the time so much.
Claude had suggestions about my initial object building function, processDataLines
:
function processDataLines(input: string): UnparsedInvoice | null {
const lines = input.split('\n').reduce((acc, line) => {
const [key, value] = line.split(': ');
if (value) acc[key.trim()] = value.trim();
return acc;
}, {} as any);
// Validate all required keys are present
const requiredKeys = ['id', 'amount', 'dueDate', 'currentDate'];
if (!requiredKeys.every(key => lines[key])) {
console.error('Missing required invoice fields');
return null;
}
return lines;
}
It’s accumulator is cool, but maybe unnecessary. I wouldn’t think to use it for this, but it really is a good use case, minus the trickiness with typing.
The every
method is useful though.
The
every()
method ofArray
instances tests whether all elements in the array pass the test implemented by the provided function. It returns a Boolean value.
I guess that the strength of this approach is the explicit key checking, rather than relying on an interface, which you can’t do within this accumulator.
However, it is better than my approach that it captures the key
data and make sure it fits, which my approach doesn’t do.
Here’s an invoice parsing strategy to try that may save time:
function safeParseInvoice(invoice: UnparsedInvoice): ParsedInvoice | null {
try {
return {
id: Number(invoice.id),
amount: Number(invoice.amount),
dueDate: new Date(invoice.dueDate),
currentDate: new Date(invoice.currentDate)
};
} catch {
return null;
}
}
I think this is honestly a case where Claude is giving me some pretty awful suggestions though. None of these type conversions are likely to error out, even when the input is invalid.
It also keep recommending this approach to date validation:
isNaN(currentDate.getTime()))
Which doesn’t really count as date validation, as the new Date
constructor is more than capable of creating an invalid interpretation of a date that happens to be a valid date.
I do like being reminded of the isNaN
operator though. I thing this could be helpful for error checking, in addition to other validation.
I also think that the try...catch
approach could be used in a cool way to catch custom errors. In the function I wrote I have:
function parseInvoice(invoice: UnparsedInvoice): ParsedInvoice | null {
const parsedId = coerceToNumber(invoice.id)
const parsedAmount = coerceToFloat(invoice.amount)
const dueDate = buildDateYYYYMMDD(invoice.dueDate)
const currentDate = buildDateYYYYMMDD(invoice.currentDate)
if (!(parsedId && parsedAmount && currentDate && dueDate)) return null
return {
id: parsedId,
amount: parsedAmount,
dueDate,
currentDate
}
}
But it would sort of cool to build in throw new Error()
into each of the validation functions I wrote, and then catch them like this:
function parseInvoice(invoice: UnparsedInvoice): ParsedInvoice | null {
try {
return {
id: coerceToNumber(invoice.id),
amount: coerceToFloat(invoice.amount),
dueDate: buildDateYYYYMMDD(invoice.dueDate),
currentDate: buildDateYYYYMMDD(invoice.currentDate)
} catch (e){
console.error(`Error parsing invoice:`, e)
return null
}
}
I love this approach and I think it would work great. I think it provides
- great error handling, which is superior to returning null
- centralized error printing and null returning, but only where we actually want to return null, leading to fewer lines of code and improved readability
- can be continued forward where instead of returning null here, we could also return an error that gets caught in an iterator at a higher level, and then returns null
To Do Still
- use external date libraries to my advantage, briefly
- rework error handling pattern so that validators return errors when validation fails
- try using a validation library as well to speed this up