The Requirements:
List the expensable items sorted by date and location given two variables: 'categories' and 'expenses'. The Categories variable provides the following information in order: Category ID, Category Name, and whether the category is expensable (Y/N). The Expenses variable provides the following information in order: Location, Date, Item Description, Cost, and Category Code. Each line should be displayed in the format "DATE: LOCATION - $TOTAL".
I put it together in a single JavaScript executable.
The code:
// Input Values
var categories = "CFE,Coffee,Y\nFD,Food,Y\nPRS,Personal,N";
var expenses = "Starbucks,3/10/2018,Iced Americano,4.28,CFE\nStarbucks,3/10/2018,Nitro Cold Brew,3.17,CFE\nStarbucks,3/10/2018,Souvineer Mug,8.19,PRS\nStarbucks,3/11/2018,Nitro Cold Brew,3.17,CFE\nHigh Point Market,3/11/2018,Iced Americano,2.75,CFE\nHigh Point Market,3/11/2018,Pastry,2.00,FD\nHigh Point Market,3/11/2018,Gift Card,10.00,PRS";
var AcceptableExpenses = GetAcceptableExpenses(GetAcceptableCategories(categories), expenses)
AcceptableExpenses.forEach (AcceptableExpense =>{
console.log(AcceptableExpense.join(''))
})
function GetAcceptableExpenses(AcceptableCategories, expenses){
var ExpensesList = expenses.split("\n")
var AcceptableExpenseTotals = []
var categoryIndex = 4
var venderIndex = 0
var dateIndex = 1
var priceIndex = 3
var venderDate = []
ExpensesList.forEach(expense => {
var expenseItemized = expense.split(',')
if ((categoryIndex < expenseItemized.length) && IsExpenseItemAcceptable(AcceptableCategories, expenseItemized[categoryIndex])){
venderDate = [expenseItemized[dateIndex], ": ", expenseItemized[venderIndex], " - $"].join('')
if ( IsVenderDateIncluded(AcceptableExpenseTotals, venderDate) ){
UpdatePrice(AcceptableExpenseTotals, venderDate, expenseItemized[priceIndex] )
}
else{
AcceptableExpenseTotals.push([venderDate, expenseItemized[priceIndex] ])
}
}
})
return AcceptableExpenseTotals
}
function IsVenderDateIncluded(AcceptableExpenseTotals, venderDate){
var venderDateIndex = 0
var IsIncluded = false
AcceptableExpenseTotals.forEach( AcceptableExpenseTotal =>{
if (AcceptableExpenseTotal[venderDateIndex] === venderDate){
IsIncluded = true
}
})
return IsIncluded
}
function UpdatePrice(AcceptableExpenseTotals, venderDate, price ){
var venderDateIndex = 0
var priceIndex = 1
AcceptableExpenseTotals.forEach( AcceptableExpenseTotal =>{
if (AcceptableExpenseTotal[venderDateIndex] === venderDate){
AcceptableExpenseTotal[priceIndex] = parseFloat( price) + parseFloat(AcceptableExpenseTotal[priceIndex])
return
}
})
}
function IsExpenseItemAcceptable(AcceptableCategories, category ){
var IsAcceptable = false
AcceptableCategories.forEach(AcceptableCategory => {
if (AcceptableCategory === category){
IsAcceptable = true
}
})
return IsAcceptable
}
function GetAcceptableCategories(categories){
var categoryList = categories.split("\n")
var AcceptableCategory = []
for (i = 0; i < categoryList.length; i++){
var categoryData = categoryList[i].split(',');
var locationOf_IsExpensible = 2;
var locationOf_Category = 0;
if (locationOf_IsExpensible < categoryData.length ){
if (categoryData[locationOf_IsExpensible]==="Y"){
AcceptableCategory.push(categoryData[locationOf_Category])
}
}
}
return AcceptableCategory
}
2 Answers 2
This code is a decent start, as it uses ES6 features like arrow functions. However it could take advantage of many Javascript features - e.g. IsExpenseItemAcceptable
could be reduced to a single line using Array.includes()
. Additionally, the index variables can be eliminated using the ES6 feature Destructuring assignment - specifically array destructuring.
Instead of lines like
var expenseItemized = expense.split(',') if ((categoryIndex < expenseItemized.length) && IsExpenseItemAcceptable(AcceptableCategories, expenseItemized[categoryIndex])){
Destructuring assignment can greatly simplify this to something like:
const [vendor, date, item, price, category] = expense.split(',');
if (category && IsExpenseItemAcceptable(AcceptableCategories, category)){
Not only is that condition shorter, it doesn't require the use of the index and is more readable (and a typo on the word "vendor" was fixed).
In the example above const
was used instead of var
. It is recommended that const
be the default keyword for initializing variables. If assignment is required, then use let
.
The function GetAcceptableCategories
could be simplified using Array.reduce()
, similar to Array.forEach()
.
The data structure could be changed from an array to a plain object - i.e. {}
to provide a mapping of vendor and date combinations to prices. This would allow the elimination of the functions updatePrice
and IsVenderDateIncluded
because the loop could be simplified to:
if (category && IsExpenseItemAcceptable(AcceptableCategories, category)){
const vendorDate = [date, ": ", vendor, " - $"].join('')
if (vendorDate in AcceptableExpenseTotals) {
AcceptableExpenseTotals[vendorDate] += parseFloat(price);
}
else{
AcceptableExpenseTotals[vendorDate] = parseFloat(price);
}
}
This would require reformatting the output - e.g.
for (const [vendorDate, price] of Object.entries(AcceptableExpenses)) {
console.log(vendorDate, price);
}
-
\$\begingroup\$ Thanks for your help. I know the checkmark and point should be enough thanks but I wanted to preface this note with thanks before I bring up the smallest of notes: I looked up vender and vendor because while I am a terrible speller it was not actually my spelling... So it turns out there are 2 words, vender and vendor. From what I gather vender is retail and vendor is manufacture supply so in this case vender was probably the correct choice. \$\endgroup\$amalgamate– amalgamate2020年06月04日 16:27:11 +00:00Commented Jun 4, 2020 at 16:27
1) I would like to add that the solution is not using object-oriented. Which would add alot to the readability and extensibility of the code. I think practically its a better as a solution then "free style - incremental programming".
The main object can be for example: - ExpensesSheet or ExpensesCalculator - You can pass proper formatted input object into it - You can set up filters - You can execute the query
2) You should consider translating long statements into functions that can be read as English
3) ExceptableCategories can be a map instead of an array to lookup isCategoryAcceptable in O(1)
4) var is old and considered bad way to define variables - js linters will recommend using const or let
i.e.: Here's a translation of your code to object-oriented style.
Note: To hide the private functions from the class interface I buried them as functions inside functions, that is not a style recommendation, just a solution I picked on the way.
class ExpensesCalculator {
constructor(categories, expenses) {
this.expensibleCateogries = new Map();
this.expensibleExpenses = {};
initExpensibleCategories(this, categories);
initExpensibleExpenses(this, expenses);
calculateTotal(this);
function initExpensibleCategories(_this, categories) {
categories.split('\n').forEach(category => {
const [categoryID, categoryName, isExpensable] = category.split(',');
if(isExpensable) {
_this.expensibleCateogries.set(categoryID, categoryName);
}
})
}
function initExpensibleExpenses(_this, expenses) {
expenses.split('\n').forEach(expense => {
const [vendor, date, item, price, category] = expense.split(',');
if (_this.expensibleCateogries.has(category)) {
if(!_this.expensibleExpenses[date]) {
_this.expensibleExpenses[date] = {};
}
if(!_this.expensibleExpenses[date][vendor]) {
_this.expensibleExpenses[date][vendor] = 0;
}
_this.expensibleExpenses[date][vendor] += parseFloat(price);
}
})
}
function calculateTotal(_this) {
_this.result = [];
Object.keys(_this.expensibleExpenses).forEach(ddate => {
Object.keys(_this.expensibleExpenses[ddate]).forEach(vendor=>{
_this.result.push({
date: new Date(ddate),
vendor,
total: _this.expensibleExpenses[ddate][vendor]})
})
})
}
}
printReport() {
this.result
.sort(byDateAndVedor)
.forEach(item=>{
console.log(`${getFormattedDate(item.date)}: ${item.vendor} - ${item.total}`);
})
function byDateAndVedor(a,b) {
return a.date - b.date === 0 ? // if date is same
('' + a.vendor).localeCompare(b.vendor) : // then sort by vendor
a.date - b.date // otherwise soft by date
}
function getFormattedDate(date) {
const dateTimeFormat =
new Intl.DateTimeFormat('en', { year: 'numeric', month: '2-digit', day: '2-digit' });
const [{ value: month },,{ value: day },,{ value: year }] = dateTimeFormat.formatToParts(date);
return `${month}/${day}/${year}`;
}
}
}
// Input Values
const categories = "CFE,Coffee,Y\nFD,Food,Y\nPRS,Personal,N";
const expenses = "Starbucks,3/10/2018,Iced Americano,4.28,CFE\nStarbucks,3/10/2018,Nitro Cold Brew,3.17,CFE\nStarbucks,3/10/2018,Souvineer Mug,8.19,PRS\nStarbucks,3/11/2018,Nitro Cold Brew,3.17,CFE\nHigh Point Market,3/11/2018,Iced Americano,2.75,CFE\nHigh Point Market,3/11/2018,Pastry,2.00,FD\nHigh Point Market,3/11/2018,Gift Card,10.00,PRS";
const expensesCalculator = new ExpensesCalculator(categories, expenses);
expensesCalculator.printReport();
Explore related questions
See similar questions with these tags.