This commit is contained in:
Martin Donnelly 2020-08-11 12:55:43 +01:00
commit e6b7c987d8
15 changed files with 5379 additions and 0 deletions

115
.gitignore vendored Normal file
View File

@ -0,0 +1,115 @@
# Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# 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
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://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/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# End of https://www.toptal.com/developers/gitignore/api/node
node_modules

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/coding-challenge.iml" filepath="$PROJECT_DIR$/.idea/coding-challenge.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

104
.idea/workspace.xml Normal file
View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="8f59754f-8b0b-4496-b8f6-b19be929e084" name="Default Changelist" comment="">
<change afterPath="$PROJECT_DIR$/tests/buyer.test.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Buyer.js" beforeDir="false" afterPath="$PROJECT_DIR$/Buyer.js" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="JavaScript File" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectId" id="1fwjsJ2NtOTdTtAo0YfeKXnfbHz" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="WebServerToolWindowFactoryState" value="true" />
<property name="dart.analysis.tool.window.visible" value="false" />
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
<property name="nodejs.jest.jest_package" value="$PROJECT_DIR$/node_modules/jest" />
<property name="settings.editor.selected.configurable" value="reference.settingsdialog.IDE.editor.colors" />
</component>
<component name="RunManager" selected="Jest.buyer.test.js">
<configuration name="Buyer" type="JavaScriptTestRunnerJest" temporary="true" nameIsGenerated="true">
<node-interpreter value="$USER_HOME$/.nvm/versions/node/v12.18.0/bin/node" />
<node-options value="" />
<jest-package value="$PROJECT_DIR$/node_modules/jest" />
<working-dir value="$PROJECT_DIR$" />
<envs />
<scope-kind value="SUITE" />
<test-file value="$PROJECT_DIR$/tests/buyer.test.js" />
<test-names>
<test-name value="Buyer" />
</test-names>
<method v="2" />
</configuration>
<configuration name="buyer.test.js" type="JavaScriptTestRunnerJest" temporary="true" nameIsGenerated="true">
<node-interpreter value="$USER_HOME$/.nvm/versions/node/v12.18.0/bin/node" />
<node-options value="" />
<jest-package value="$PROJECT_DIR$/node_modules/jest" />
<working-dir value="$PROJECT_DIR$" />
<envs />
<scope-kind value="TEST_FILE" />
<test-file value="$PROJECT_DIR$/tests/buyer.test.js" />
<method v="2" />
</configuration>
<recent_temporary>
<list>
<item itemvalue="Jest.buyer.test.js" />
<item itemvalue="Jest.Buyer" />
</list>
</recent_temporary>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="8f59754f-8b0b-4496-b8f6-b19be929e084" name="Default Changelist" comment="" />
<created>1597136254764</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1597136254764</updated>
<workItem from="1597136255857" duration="11000" />
<workItem from="1597136289338" duration="7691000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="WindowStateProjectService">
<state width="2463" height="519" key="GridCell.Tab.0.bottom" timestamp="1597146833789">
<screen x="0" y="29" width="2560" height="1411" />
</state>
<state width="2463" height="519" key="GridCell.Tab.0.bottom/0.29.2560.1411@0.29.2560.1411" timestamp="1597146833789" />
<state width="2463" height="519" key="GridCell.Tab.0.center" timestamp="1597146833789">
<screen x="0" y="29" width="2560" height="1411" />
</state>
<state width="2463" height="519" key="GridCell.Tab.0.center/0.29.2560.1411@0.29.2560.1411" timestamp="1597146833789" />
<state width="2463" height="519" key="GridCell.Tab.0.left" timestamp="1597146833788">
<screen x="0" y="29" width="2560" height="1411" />
</state>
<state width="2463" height="519" key="GridCell.Tab.0.left/0.29.2560.1411@0.29.2560.1411" timestamp="1597146833788" />
<state width="2463" height="519" key="GridCell.Tab.0.right" timestamp="1597146833789">
<screen x="0" y="29" width="2560" height="1411" />
</state>
<state width="2463" height="519" key="GridCell.Tab.0.right/0.29.2560.1411@0.29.2560.1411" timestamp="1597146833789" />
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/coding_challenge$buyer_test_js.info" NAME="buyer.test.js Coverage Results" MODIFIED="1597143325899" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="JestJavaScriptTestRunnerCoverage" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" />
</component>
</project>

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"
}
]
}

83
Buyer.js Normal file
View File

@ -0,0 +1,83 @@
class Buyer {
constructor(market) {
this.market = market;
}
/**
* Build an order preference table for particular product
* @param product
*/
buildOrderPreference(product) {
let sellers = [];
// build up the initial list
this.market.sellers.forEach(seller => {
if (seller.inventory.hasOwnProperty(product)) {
const price = seller.quote(product);
sellers.push({id:seller.id, price:price})
}
});
// sort by best price
sellers.sort((a, b) => {
return a.price - b.price;
})
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;
}
/**
* 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) {
let sellerPreference = this.buildOrderPreference(product);
console.log(sellerPreference);
while(sellerPreference.length > 0) {
let seller = sellerPreference.shift();
}
throw Error("Not Implemented");
}
/**
* 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) {
throw Error("Not Implemented");
}
}
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;

70
Seller.js Normal file
View File

@ -0,0 +1,70 @@
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
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.completelyFill(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}

4669
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View File

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

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

@ -0,0 +1,47 @@
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", () => {
let buyer = new Buyer(market);
expect(buyer.getBestPrice('Apples')).toEqual(4.25);
});
it("should return best price that only 2 have", () => {
let buyer = new Buyer(market);
expect(buyer.getBestPrice('Grapes')).toEqual(21);
});
it("should return best price that only 1 has", () => {
let buyer = new Buyer(market);
expect(buyer.getBestPrice('Mangosteen')).toEqual(100.0);
});
it("should return 0 when fruit doesnt exist", () => {
let buyer = new Buyer(market);
expect(buyer.getBestPrice('Kumquat')).toEqual(0);
});
it("fill", () => {
let buyer = new Buyer(market);
expect(buyer.fillWithBestPrices('Apples', 50)).toEqual(0);
});
});

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);
})
})