This commit is contained in:
Martin Donnelly 2020-08-12 09:28:37 +01:00
commit fab442ea1d
12 changed files with 5920 additions and 0 deletions

55
.eslintrc.json Normal file
View File

@ -0,0 +1,55 @@
{
"parserOptions": {
"ecmaVersion": 2017,
"sourceType": "module",
"ecmaFeatures": {
"jsx": false
}
},
"env": {
"browser": true,
"node": true,
"es2017": true
},
"rules": {
"arrow-spacing": "error",
"block-scoped-var": "error",
"block-spacing": "error",
"brace-style": ["error", "stroustrup", {}],
"camelcase": "error",
"comma-dangle": ["error", "never"],
"comma-spacing": ["error", { "before": false, "after": true }],
"comma-style": [1, "last"],
"consistent-this": [1, "_this"],
"curly": [1, "multi"],
"eol-last": 1,
"eqeqeq": 1,
"func-names": 1,
"indent": ["error", 2, { "SwitchCase": 1 }],
"lines-around-comment": ["error", { "beforeBlockComment": true, "allowArrayStart": true }],
"max-len": [1, 180, 2], // 2 spaces per tab, max 80 chars per line
"new-cap": 1,
"newline-before-return": "error",
"no-array-constructor": 1,
"no-inner-declarations": [1, "both"],
"no-mixed-spaces-and-tabs": 1,
"no-multi-spaces": 2,
"no-new-object": 1,
"no-shadow-restricted-names": 1,
"object-curly-spacing": ["error", "always"],
"padded-blocks": ["error", { "blocks": "never", "switches": "always" }],
"prefer-const": "error",
"prefer-template": "error",
"one-var": 0,
"quote-props": ["error", "always"],
"quotes": [1, "single"],
"radix": 1,
"semi": [1, "always"],
"space-before-blocks": [1, "always"],
"space-infix-ops": 1,
"vars-on-top": 1,
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 1 }],
"spaced-comment": ["error", "always", { "markers": ["/"] }]
}
}

149
.gitignore vendored Normal file
View File

@ -0,0 +1,149 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea/
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Elastic Beanstalk Files
.elasticbeanstalk/*
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml
/src/bundle.js
/src/bundle.js.map
/live/
!/output/
/db/jobs.db

29
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,29 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "main",
"program": "${workspaceFolder}/main.js"
},
{
"type": "node",
"name": "vscode-jest-tests",
"request": "launch",
"args": [
"--runInBand"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
}
]
}

154
Buyer.js Normal file
View File

@ -0,0 +1,154 @@
class Buyer {
constructor(market) {
this.market = market;
}
/**
* Build a list of the sellers
* @param product
* @returns {*}
*/
buildSellerList(product) {
const sellers = [];
this.market.sellers.forEach((seller, i) => {
if (seller.inventory.hasOwnProperty(product)) {
const price = seller.quote(product);
sellers.push({ 'id': seller.id, 'price': price, 'index': i, 'quantity': seller.inventory[product].quantity });
}
});
return sellers;
}
/**
* Build an order preference table for particular product
* @param product
*/
buildOrderPrefBestPrice(product) {
const sellers = this.buildSellerList(product);
// sort by best price
sellers.sort((a, b) => {
return a.price - b.price;
});
return sellers;
}
/**
* Sort the list based on Delivery delay
* @param product
* @returns {*}
*/
buildOrderPrefFastFill(product) {
const sellers = this.buildSellerList(product);
// sort by best price
sellers.sort((a, b) => {
return a.deliveryWait - b.deliveryWait;
});
return sellers;
}
/**
* Sort the list based on item quantity
* @param product
* @returns {*}
*/
buildOrderPrefMostFill(product) {
const sellers = this.buildSellerList(product);
// sort by best price
sellers.sort((a, b) => {
return b.quantity - a.quantity;
});
return sellers;
}
/**
* This method should get the best price for a given product
* across all sellers
*/
getBestPrice(product) {
let lowestPrice = null;
this.market.sellers.forEach(seller => {
if (seller.inventory.hasOwnProperty(product)) {
const price = seller.quote(product);
if (lowestPrice === null || lowestPrice > price) lowestPrice = price;
}
});
return lowestPrice || 0;
}
/**
* Do the actual purchases based on preference list
* @param preference
* @param product
* @param quantity
* @returns {number}
*/
doPurchases(preference, product, quantity) {
const sellerPreference = [...preference];
let total = 0;
let wantedQuantity = quantity;
const receipt = [];
while (sellerPreference.length > 0 && wantedQuantity > 0) {
const seller = sellerPreference.shift();
const r = this.market.sellers[seller.index].sell(product, wantedQuantity);
wantedQuantity = (wantedQuantity - r.boughtQuantity) < 0 ? 0 : (wantedQuantity - r.boughtQuantity);
receipt.push(r);
}
if (receipt.length > 1)
total = receipt.reduce((a, cv) => {
return (typeof a === 'number' ? a : a.cost) + cv.cost;
});
else if (receipt.length === 1) total = receipt[0].cost;
return total;
}
/**
* This method should optimise price when filling an order
* if the quantity is greater than any single seller can accomodate
* then the next cheapest seller should be used.
*/
fillWithBestPrices(product, quantity) {
const sellerPreference = this.buildOrderPrefBestPrice(product);
return this.doPurchases(sellerPreference, product, quantity);
}
/**
* This method should optimise for sellers with the largest inventory when filling an order
* if the quantity is greater than any single seller can accomodate
* then the next largest seller should be used.
* if multiple sellers have the same amount of inventory
* you should use the cheaper of the two.
*/
fillWithLargestSellers(product, quantity) {
const sellerPreference = this.buildOrderPrefMostFill(product);
return this.doPurchases(sellerPreference, product, quantity);
}
/**
* This fulfils orders based on time to deliver
* @param product
* @param quantity
* @returns {number}
*/
quicklyFill(product, quantity) {
const sellerPreference = this.buildOrderPrefFastFill(product);
return this.doPurchases(sellerPreference, product, quantity);
}
}
module.exports = { Buyer };

20
Market.js Normal file
View File

@ -0,0 +1,20 @@
const { Subject, interval } = require('rxjs');
class Market {
constructor(sellers) {
this.sellers = sellers;
this.observable = new Subject();
this.observable.subscribe({
next: (v) => this.tick()
});
interval(5000).subscribe(v => this.observable.next(v));
}
tick(){
this.sellers.forEach(seller => {
seller.tick()
});
}
}
exports.Market = Market;

71
Seller.js Normal file
View File

@ -0,0 +1,71 @@
const stream = require('stream');
const rand = require('random-seed');
function getExpectedChange(generator) {
return generator(100) / 100;
}
function getDeliveries(iProduct, generator) {
let fluctuation = getExpectedChange(generator);
let newDeliveries = fluctuation * iProduct.startingQuantity;
iProduct.quantity += iProduct.quantity + newDeliveries;
return iProduct;
}
class Seller {
constructor(inventory, id = "Safeway", deliveryWait = 5) {
this.inventory = inventory;
this.deliveryWait = deliveryWait;
this.random_generator = rand(id);
this.id = id;
for (let [key, value] of Object.entries(inventory)) {
value.startingQuantity = value.quantity;
value.priceHistory = [value.price];
value.stingyness = 0;
}
}
quote(product) {
const inventory = this.inventory[product];
return inventory.price;
}
calculatePriceChange(product){
const inventory = this.inventory[product];
const v = 0.1
const ec = getExpectedChange(this.random_generator);
const alpha = inventory.startingQuantity
const beta = inventory.quantity
// console.log(`${this.id} alpha: ${alpha} // beta: ${beta} // ec: ${ec}`);
const inv_based_change = Math.log10(beta / alpha) * (-v);
const sentimentChange = inv_based_change + ((ec - 0.5)*v)
return sentimentChange;
}
sell(product, buyQuantity) {
const inventory = this.inventory[product];
const boughtQuantity = buyQuantity > inventory.quantity ? inventory.quantity : buyQuantity;
const cost = boughtQuantity * this.quote(product);
inventory.quantity -= boughtQuantity;
inventory.stingyness = 1 - inventory.quantity / inventory.startingQuantity;
this.tick();
return {boughtQuantity, cost};
}
tick() {
for (let [product, value] of Object.entries(this.inventory)) {
let inventory = value;
const isReadyForDelivery = (inventory.priceHistory.length % this.deliveryWait) == 0;
if (isReadyForDelivery) {
inventory = getDeliveries(inventory, this.random_generator);
}
let chg = this.calculatePriceChange(product);
inventory.price = inventory.price + (inventory.price*chg)
inventory.priceHistory.push(inventory.price);
}
}
}
module.exports = {Seller}

27
main.js Normal file
View File

@ -0,0 +1,27 @@
const {asda,costco,budgens} = require("./marketplace");
const {Market} = require("./Market");
const { Buyer } = require("./Buyer");
function main(){
const market = new Market([asda,budgens,costco]);
let buyer = new Buyer(market);
let product = "Apples";
let quantity = 10;
buyerFunctions(product, quantity, buyer);
observeMarket(market);
};
function buyerFunctions(product, quantity, buyer){
console.log(`The best price for ${product} is ${buyer.getBestPrice(product)}`) ;
console.log(`To completely fill a order of ${quantity} ${product} costs ${buyer.fillWithBestPrices(product,quantity)}`) ;
console.log(`To buy as quickly as possible ${quantity} ${product} costs ${buyer.quicklyFill(product,quantity)}`) ;
}
function observeMarket(market){
market.observable.subscribe( (mkt) => {
console.log(`The current price of apples are ${market.sellers[0].inventory["Apples"].price}`)});
}
main();

69
marketplace.js Normal file
View File

@ -0,0 +1,69 @@
const { Market } = require("./Market");
const { Seller } = require("./Seller")
const asda = new Seller({
"Apples":{
quantity:100,
price:5.25
},
"Oranges":{
quantity:150,
price:8.0
},
"Pears":{
quantity:10,
price:15.0
},
"Banannas":{
quantity:1000,
price:10.0
}
}, "Asda", 5);
const budgens = new Seller({
"Apples":{
quantity:25,
price:4.25
},
"Oranges":{
quantity:15,
price:6.0
},
"Grapes":{
quantity:10,
price:21.0
},
"Banannas":{
quantity:100,
price:4.0
}
}, "Budgens", 1);
const costco = new Seller({
"Apples":{
quantity:250,
price:6.25
},
"Oranges":{
quantity:300,
price:10.0
},
"Grapes":{
quantity:100,
price:35.0
},
"Pears":{
quantity:100,
price:30.0
},
"Mangosteen":{
quantity:10,
price:100.0
},
"Banannas":{
quantity:100,
price:8.0
}
}, "Costco", 10);
module.exports = {asda,budgens, costco}

5162
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "codingtest",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node main.js",
"test": "jest"
},
"author": "",
"license": "ISC",
"devDependencies": {
"eslint": "^7.6.0",
"expect": "^26.0.1",
"jest": "^26.0.1"
},
"dependencies": {
"random-seed": "^0.3.0",
"rxjs": "^6.5.5"
}
}

63
tests/buyer.test.js Normal file
View File

@ -0,0 +1,63 @@
const expect = require('expect');
const { asda, costco, budgens } = require('../marketplace');
const { Market } = require('../Market');
const { Buyer } = require('../Buyer');
describe('Buyer', function () {
var market;
beforeEach(() => {
market = new Market([asda, budgens, costco]);
});
// getBestPrice
it('should return best price', () => {
const buyer = new Buyer(market);
expect(buyer.getBestPrice('Apples')).toEqual(4.25);
});
it('should return best price that only 2 have', () => {
const buyer = new Buyer(market);
expect(buyer.getBestPrice('Grapes')).toEqual(21);
});
it('should return best price that only 1 has', () => {
const buyer = new Buyer(market);
expect(buyer.getBestPrice('Mangosteen')).toEqual(100.0);
});
it('should return 0 when fruit doesnt exist', () => {
const buyer = new Buyer(market);
expect(buyer.getBestPrice('Kumquat')).toEqual(0);
});
// Filling
it('fill 10 apples', () => {
const buyer = new Buyer(market);
expect(buyer.fillWithBestPrices('Apples', 10)).toEqual(42.5);
});
it('fill 50 apples', () => {
const buyer = new Buyer(market);
expect(buyer.fillWithBestPrices('Apples', 50)).toEqual(233.60268569487857);
});
it('unable to fill 10 Kumquat', () => {
const buyer = new Buyer(market);
expect(buyer.fillWithBestPrices('Kumquat', 10)).toEqual(0);
});
// Large fill
it('Large fill 50 apples', () => {
const buyer = new Buyer(market);
expect(buyer.fillWithLargestSellers('Apples', 50)).toEqual(312.5);
});
});

100
tests/seller.test.js Normal file
View File

@ -0,0 +1,100 @@
const expect = require("expect")
const {Seller} = require("../Seller")
describe("Seller", function(){
var sellerInventory;
beforeEach(() => {
sellerInventory = {
"Apples":{
quantity:100,
price:5.25
},
"Oranges":{
quantity:150,
price:8.0
},
"Pears":{
quantity:10,
price:15.0
}
}
})
it("should reduce quantity when i sell", ()=> {
let sut = new Seller(sellerInventory)
sut.sell("Apples", 25)
expect(sut.inventory["Apples"].quantity).toEqual(75)
});
it("should cap at 0 if I sell more than i have", () =>{
let sut = new Seller(sellerInventory);
sut.sell("Apples", 105);
expect(sut.inventory["Apples"].quantity).toEqual(0);
});
it('should be maximally stingy when empty inventory', () => {
let sut = new Seller(sellerInventory);
sut.sell("Apples", 105);
expect(sut.inventory["Apples"].stingyness).toEqual(1);
});
it("should be minimally stingy when full inventory", () => {
let sut = new Seller(sellerInventory);
expect(sut.inventory["Apples"].stingyness).toEqual(0);
});
it("should be somewhat stingy when half inventory", () => {
let sut = new Seller(sellerInventory);
sut.sell("Apples", sut.inventory["Apples"].quantity/2);
expect(sut.inventory["Apples"].stingyness).toEqual(0.5);
});
it("should quote initial price on first ask", ()=>{
let sut = new Seller(sellerInventory);
expect(sut.inventory["Oranges"].price).toEqual(8.0);
});
it("should raise prices after seller buys", () =>{
let sut = new Seller(sellerInventory, "Kwiksave");
sut.sell("Oranges", sut.inventory["Oranges"].quantity/2);
expect(sut.inventory["Oranges"].price).toBeGreaterThan(8.0);
});
it("should get deliveries after seller buys once", () =>{
const deliveryCadence = 1;
let sut = new Seller(sellerInventory,"Asda",deliveryCadence);
sut.sell("Oranges", 1);
expect(sut.inventory["Oranges"].quantity).toBeGreaterThan(sut.inventory["Oranges"].startingQuantity);
});
it("should be able to set delivery schedule", () => {
const deliveryCadence = 3;
let sut = new Seller(sellerInventory,"Asda",deliveryCadence);
allOranges = sut.inventory["Oranges"].quantity;
sut.sell("Oranges", allOranges);
expect(sut.inventory["Oranges"].quantity).toEqual(0);
sut.tick("Oranges");
expect(sut.inventory["Oranges"].quantity).toEqual(0);
sut.tick("Oranges");
expect(sut.inventory["Oranges"].quantity).toBeGreaterThan(0);
});
it("should return correct receipt when seller sells single unit", () =>{
let sut = new Seller(sellerInventory);
const buyAmount = 10;
let receipt = sut.sell("Oranges", buyAmount);
expect(receipt.cost).toEqual(sut.inventory["Oranges"].priceHistory[0] * buyAmount);
expect(receipt.boughtQuantity).toEqual(buyAmount);
})
it("should return correct receipt when seller sells all stock", () =>{
let sut = new Seller(sellerInventory);
const overboughtAmount = 1000;
const expectedBuyAmount = sut.inventory["Oranges"].startingQuantity;
let receipt = sut.sell("Oranges", overboughtAmount);
expect(receipt.cost).toEqual(sut.inventory["Oranges"].priceHistory[0] * expectedBuyAmount);
expect(receipt.boughtQuantity).toEqual(expectedBuyAmount);
})
})