init
This commit is contained in:
commit
83007b6f04
32
.editorconfig
Normal file
32
.editorconfig
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
; http://editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.txt]
|
||||||
|
insert_final_newline = false
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.py]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.m]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 8
|
||||||
|
|
||||||
|
[*.{js,json,ts}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
72
.eslintrc.json
Normal file
72
.eslintrc.json
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2020,
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true,
|
||||||
|
"es2020": true,
|
||||||
|
"mocha": true
|
||||||
|
},
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"extends": ["eslint:recommended"],
|
||||||
|
"rules": {
|
||||||
|
"no-console": 2,
|
||||||
|
"no-with": 2,
|
||||||
|
"brace-style": [
|
||||||
|
2,
|
||||||
|
"1tbs",
|
||||||
|
{
|
||||||
|
"allowSingleLine": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-mixed-spaces-and-tabs": 2,
|
||||||
|
"one-var": [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
"uninitialized": "always",
|
||||||
|
"initialized": "never"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"quote-props": [2, "as-needed"],
|
||||||
|
"key-spacing": [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
"beforeColon": false,
|
||||||
|
"afterColon": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"space-unary-ops": [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
"nonwords": false,
|
||||||
|
"overrides": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"space-before-function-paren": [2, "never"],
|
||||||
|
"space-in-parens": [2, "never"],
|
||||||
|
"no-trailing-spaces": 2,
|
||||||
|
"max-len": [2, 160],
|
||||||
|
"camelcase": 0,
|
||||||
|
"curly": [2, "all"],
|
||||||
|
"keyword-spacing": [2, {}],
|
||||||
|
"spaced-comment": [2, "always"],
|
||||||
|
"space-infix-ops": 2,
|
||||||
|
"space-before-blocks": [2, "always"],
|
||||||
|
"comma-dangle": 0,
|
||||||
|
"no-else-return": 0,
|
||||||
|
"indent": [
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
"SwitchCase": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"linebreak-style": [2, "unix"],
|
||||||
|
"quotes": [2, "single"]
|
||||||
|
}
|
||||||
|
}
|
182
.gitignore
vendored
Normal file
182
.gitignore
vendored
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### Archives template
|
||||||
|
# It's better to unpack these files and commit the raw source because
|
||||||
|
# git has its own built in compression methods.
|
||||||
|
*.7z
|
||||||
|
*.jar
|
||||||
|
*.rar
|
||||||
|
*.zip
|
||||||
|
*.gz
|
||||||
|
*.bzip
|
||||||
|
*.bz2
|
||||||
|
*.xz
|
||||||
|
*.lzma
|
||||||
|
*.cab
|
||||||
|
|
||||||
|
#packing-only formats
|
||||||
|
*.iso
|
||||||
|
*.tar
|
||||||
|
|
||||||
|
#package management formats
|
||||||
|
*.dmg
|
||||||
|
*.xpi
|
||||||
|
*.gem
|
||||||
|
*.egg
|
||||||
|
*.deb
|
||||||
|
*.rpm
|
||||||
|
*.msi
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
### Windows template
|
||||||
|
# Windows image file caches
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
|
||||||
|
# Folder config file
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# Recycle Bin used on file shares
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
|
# Windows Installer files
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
|
||||||
|
# Windows shortcuts
|
||||||
|
*.lnk
|
||||||
|
### OSX template
|
||||||
|
.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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
## Directory-based project format:
|
||||||
|
.idea/
|
||||||
|
# if you remove the above rule, at least ignore the following:
|
||||||
|
|
||||||
|
# User-specific stuff:
|
||||||
|
# .idea/workspace.xml
|
||||||
|
# .idea/tasks.xml
|
||||||
|
# .idea/dictionaries
|
||||||
|
|
||||||
|
# Sensitive or high-churn files:
|
||||||
|
# .idea/dataSources.ids
|
||||||
|
# .idea/dataSources.xml
|
||||||
|
# .idea/sqlDataSources.xml
|
||||||
|
# .idea/dynamic.xml
|
||||||
|
# .idea/uiDesigner.xml
|
||||||
|
|
||||||
|
# Gradle:
|
||||||
|
# .idea/gradle.xml
|
||||||
|
# .idea/libraries
|
||||||
|
|
||||||
|
# Mongo Explorer plugin:
|
||||||
|
# .idea/mongoSettings.xml
|
||||||
|
|
||||||
|
## File-based project format:
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
## Plugin-specific files:
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
### Node template
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directory
|
||||||
|
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
|
||||||
|
node_modules
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
### VisualStudioCode template
|
||||||
|
.settings
|
||||||
|
|
||||||
|
### Xcode template
|
||||||
|
# Xcode
|
||||||
|
#
|
||||||
|
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||||
|
|
||||||
|
## Build generated
|
||||||
|
build/
|
||||||
|
DerivedData
|
||||||
|
|
||||||
|
## Various settings
|
||||||
|
*.pbxuser
|
||||||
|
!default.pbxuser
|
||||||
|
*.mode1v3
|
||||||
|
!default.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
!default.mode2v3
|
||||||
|
*.perspectivev3
|
||||||
|
!default.perspectivev3
|
||||||
|
xcuserdata
|
||||||
|
|
||||||
|
## Other
|
||||||
|
*.xccheckout
|
||||||
|
*.moved-aside
|
||||||
|
*.xcuserstate
|
||||||
|
|
||||||
|
dist
|
||||||
|
.nyc_output
|
||||||
|
.prettierc
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
166
lib/carpark.js
Normal file
166
lib/carpark.js
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
The following charges are used:
|
||||||
|
|
||||||
|
Short Stay
|
||||||
|
£1.10 per hour between 8am and 6pm on weekdays, free outside of these times.
|
||||||
|
Visits need not be whole hours and can last more than one day.
|
||||||
|
|
||||||
|
Long Stay
|
||||||
|
£7.50 per day or part day including weekends, so the minimum charge will be for one day.
|
||||||
|
|
||||||
|
|
||||||
|
A stay entirely outside of a chargeable period will return £0.00
|
||||||
|
A short stay from 07/09/2017 16:50:00 to 09/09/2017 19:15:00 would cost £12.28
|
||||||
|
Thursday => Saturday ( 1504803000 => 1504984500)
|
||||||
|
|
||||||
|
Thursday: 1h 10m = 1.28
|
||||||
|
Friday: 10h = 11.00
|
||||||
|
Saturday: free
|
||||||
|
|
||||||
|
A long stay from 07/09/2017 07:50:00 to 09/09/2017 05:20:00 would cost £22.50
|
||||||
|
Thursday => Saturday ( 1504770600 => 1504934400)
|
||||||
|
|
||||||
|
Thursday: 1 = 7.5
|
||||||
|
Friday: 1
|
||||||
|
Saturday: 1
|
||||||
|
Total: 22.50
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
hour = 3600 seconds
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
1604414190894
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fecha = require('fecha');
|
||||||
|
|
||||||
|
class Carpark {
|
||||||
|
#_startDT;
|
||||||
|
#_endDT;
|
||||||
|
#_dayLength = 60 * 60 * 24;
|
||||||
|
#_hourLength = 60 * 60;
|
||||||
|
#_validDays = [1, 2, 3, 4, 5]; // Monday, Tuesday, Wednesday, Thursday, Friday
|
||||||
|
#_validStart = this.#_hourLength * 8; // 8AM // 08:00
|
||||||
|
#_validEnd = this.#_hourLength * 18; // 6PM // 18:00
|
||||||
|
#_longPrice = 7.5;
|
||||||
|
#_shortPrice = 1.1;
|
||||||
|
#_secondPrice = this.#_shortPrice / this.#_hourLength;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a specific timestamp is a valid date
|
||||||
|
* @param workTime
|
||||||
|
* @returns {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_isValidTime(workTime) {
|
||||||
|
const day = new Date(workTime * 1000).getDay();
|
||||||
|
|
||||||
|
if (this.#_validDays.indexOf(day) > -1) {
|
||||||
|
const dayBase = ~~(workTime / this.#_dayLength) * this.#_dayLength;
|
||||||
|
const dayPosition = workTime - dayBase;
|
||||||
|
|
||||||
|
return dayPosition >= this.#_validStart && dayPosition < this.#_validEnd - 1;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate short term parking costs
|
||||||
|
* @returns {number}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_calcShortTerm() {
|
||||||
|
let startSeconds = this.#_startDT.getTime() / 1000;
|
||||||
|
let endSeconds = this.#_endDT.getTime() / 1000;
|
||||||
|
let workTime, total;
|
||||||
|
let validHours = 0;
|
||||||
|
let startHourPart = 0;
|
||||||
|
|
||||||
|
// Calculate Start Hour
|
||||||
|
let startBaseHour = ~~(startSeconds / this.#_hourLength) * this.#_hourLength;
|
||||||
|
|
||||||
|
// Calculate End Hour
|
||||||
|
let endBaseHour = ~~(endSeconds / this.#_hourLength) * this.#_hourLength;
|
||||||
|
|
||||||
|
// Check if the very first portion of the hour is valid
|
||||||
|
if (this._isValidTime(startSeconds)) {
|
||||||
|
startHourPart = this.#_hourLength - (startSeconds - startBaseHour);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go to the next hour after the initial start
|
||||||
|
workTime = startBaseHour + this.#_hourLength;
|
||||||
|
|
||||||
|
// If there's more of an hours worth of time then loop through the next hours
|
||||||
|
if (endBaseHour - workTime > this.#_hourLength) {
|
||||||
|
do {
|
||||||
|
validHours = validHours + (this._isValidTime(workTime) ? 1 : 0);
|
||||||
|
workTime = workTime + this.#_hourLength;
|
||||||
|
} while (workTime <= endBaseHour);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum it all
|
||||||
|
total = startHourPart * this.#_secondPrice + validHours * this.#_shortPrice;
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate long term parking costs
|
||||||
|
* @returns {number}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_calcLongTerm() {
|
||||||
|
// convert to time stamps and make them second based
|
||||||
|
const startSeconds = this.#_startDT.getTime() / 1000;
|
||||||
|
const endSeconds = this.#_endDT.getTime() / 1000;
|
||||||
|
|
||||||
|
// Very start of the first day
|
||||||
|
const startBaseSeconds = ~~(startSeconds / this.#_dayLength) * this.#_dayLength;
|
||||||
|
|
||||||
|
// Very end of the second day
|
||||||
|
const endBaseSeconds = ~~(endSeconds / this.#_dayLength) * this.#_dayLength + this.#_dayLength;
|
||||||
|
|
||||||
|
// Subtract the start seconds from the end seconds
|
||||||
|
const distance = endBaseSeconds - startBaseSeconds;
|
||||||
|
|
||||||
|
// divide them by number of seconds in a day and multiply by cost
|
||||||
|
|
||||||
|
return (distance / this.#_dayLength) * this.#_longPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The public calculate method
|
||||||
|
* @param start
|
||||||
|
* @param end
|
||||||
|
* @param mode
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
calculate(start, end, mode = 'longterm') {
|
||||||
|
let workVal = 0.0;
|
||||||
|
this.#_startDT = fecha.parse(start, 'DD/MM/YYYY HH:mm:ssZ');
|
||||||
|
this.#_endDT = fecha.parse(end, 'DD/MM/YYYY HH:mm:ssZ');
|
||||||
|
|
||||||
|
switch (mode.toLowerCase()) {
|
||||||
|
case 'short':
|
||||||
|
case 'shortterm':
|
||||||
|
workVal = this._calcShortTerm();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'long':
|
||||||
|
case 'longterm':
|
||||||
|
workVal = this._calcLongTerm();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseFloat(workVal.toFixed(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Carpark;
|
4378
package-lock.json
generated
Normal file
4378
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
package.json
Normal file
32
package.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "hotdocs",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Parking Charge Calculator",
|
||||||
|
"main": "lib/carpark.js",
|
||||||
|
"scripts": {
|
||||||
|
"test:js": "mocha test/**/*.js",
|
||||||
|
"test:ts": "mocha -r ts-node/register test/**/*.ts",
|
||||||
|
"lint": "eslint . --ext .ts",
|
||||||
|
"coverage": "nyc npm run test:js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Martin Donnelly",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/chai": "^4.2.14",
|
||||||
|
"@types/mocha": "^8.0.3",
|
||||||
|
"@types/node": "^14.14.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.6.1",
|
||||||
|
"@typescript-eslint/parser": "^4.6.1",
|
||||||
|
"chai": "^4.2.0",
|
||||||
|
"eslint": "^7.12.1",
|
||||||
|
"expect.js": "^0.3.1",
|
||||||
|
"mocha": "^8.2.1",
|
||||||
|
"nyc": "^15.1.0",
|
||||||
|
"ts-node": "^9.0.0",
|
||||||
|
"typings": "^2.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"fecha": "^4.2.0"
|
||||||
|
}
|
||||||
|
}
|
68
test/carpark-ts.ts
Normal file
68
test/carpark-ts.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import Carpark from '../ts-lib/carpark-calc';
|
||||||
|
|
||||||
|
import { expect } from 'chai';
|
||||||
|
// const expect = require('expect.js');
|
||||||
|
|
||||||
|
|
||||||
|
describe('Carpark Calculator', () => {
|
||||||
|
let carPark: Carpark;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
carPark = new Carpark();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return 0.00 outside of a chargeable period', () => {
|
||||||
|
|
||||||
|
const start = '09/09/2017 05:20:00';
|
||||||
|
const end = '09/09/2017 19:15:00';
|
||||||
|
|
||||||
|
expect(carPark.calculate(start, end, 'short')).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('A specific short stay should cost £12.28', () => {
|
||||||
|
|
||||||
|
const start = '07/09/2017 16:50:00';
|
||||||
|
const end = '09/09/2017 19:15:00';
|
||||||
|
|
||||||
|
expect(carPark.calculate(start, end, 'short')).to.equal(12.28);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('A specific long stay should cost £22.50', () => {
|
||||||
|
|
||||||
|
const start = '07/09/2017 07:50:00';
|
||||||
|
const end = '09/09/2017 05:20:00';
|
||||||
|
|
||||||
|
expect(carPark.calculate(start, end, 'long')).to.equal(22.50);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('A very specific long stay should cost £15.00', () => {
|
||||||
|
|
||||||
|
const start = '24/10/2020 10:00:00';
|
||||||
|
const end = '25/10/2020 10:00:00';
|
||||||
|
|
||||||
|
expect(carPark.calculate(start, end, 'long')).to.equal(15.00);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('A single day of long stay should cost £7.50', () => {
|
||||||
|
|
||||||
|
const start = '24/10/2020 10:00:00';
|
||||||
|
const end = '24/10/2020 23:59:00';
|
||||||
|
|
||||||
|
expect(carPark.calculate(start, end, 'long')).to.equal(7.50);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('A single week of long stay should cost £52.50', () => {
|
||||||
|
|
||||||
|
const start = '24/10/2020 10:00:00';
|
||||||
|
const end = '30/10/2020 10:00:00';
|
||||||
|
|
||||||
|
expect(carPark.calculate(start, end, 'long')).to.equal(52.50);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
53
test/carpark.js
Normal file
53
test/carpark.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
const Carpark = require('../lib/carpark');
|
||||||
|
|
||||||
|
const expect = require('expect.js');
|
||||||
|
|
||||||
|
describe('Carpark Calculator', () => {
|
||||||
|
let carPark;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
carPark = new Carpark();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return 0.00 outside of a chargeable period', () => {
|
||||||
|
const start = '09/09/2017 05:20:00';
|
||||||
|
const end = '09/09/2017 19:15:00';
|
||||||
|
|
||||||
|
expect(carPark.calculate(start, end, 'short')).to.be(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('A specific short stay should cost £12.28', () => {
|
||||||
|
const start = '07/09/2017 16:50:00';
|
||||||
|
const end = '09/09/2017 19:15:00';
|
||||||
|
|
||||||
|
expect(carPark.calculate(start, end, 'short')).to.be(12.28);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('A specific long stay should cost £22.50', () => {
|
||||||
|
const start = '07/09/2017 07:50:00';
|
||||||
|
const end = '09/09/2017 05:20:00';
|
||||||
|
|
||||||
|
expect(carPark.calculate(start, end, 'long')).to.be(22.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('A very specific long stay should cost £15.00', () => {
|
||||||
|
const start = '24/10/2020 10:00:00';
|
||||||
|
const end = '25/10/2020 10:00:00';
|
||||||
|
|
||||||
|
expect(carPark.calculate(start, end, 'long')).to.be(15.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('A single day of long stay should cost £7.50', () => {
|
||||||
|
const start = '24/10/2020 10:00:00';
|
||||||
|
const end = '24/10/2020 23:59:00';
|
||||||
|
|
||||||
|
expect(carPark.calculate(start, end, 'long')).to.be(7.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('A single week of long stay should cost £52.50', () => {
|
||||||
|
const start = '24/10/2020 10:00:00';
|
||||||
|
const end = '30/10/2020 10:00:00';
|
||||||
|
|
||||||
|
expect(carPark.calculate(start, end, 'long')).to.be(52.5);
|
||||||
|
});
|
||||||
|
});
|
123
ts-lib/carpark-calc.ts
Normal file
123
ts-lib/carpark-calc.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { parse } from 'fecha';
|
||||||
|
|
||||||
|
class Carpark {
|
||||||
|
private _startDT: Date;
|
||||||
|
private _endDT: Date;
|
||||||
|
private _dayLength: number = 60 * 60 * 24;
|
||||||
|
private _hourLength: number = 60 * 60;
|
||||||
|
private _validDays: number[] = [1, 2, 3, 4, 5]; // Monday, Tuesday, Wednesday, Thursday, Friday
|
||||||
|
private _validStart: number = this._hourLength * 8; // 8AM // 08:00
|
||||||
|
private _validEnd: number = this._hourLength * 18; // 6PM // 18:00
|
||||||
|
private _longPrice = 7.5;
|
||||||
|
private _shortPrice = 1.1;
|
||||||
|
private _secondPrice: number = this._shortPrice / this._hourLength;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a specific timestamp is a valid date
|
||||||
|
* @param workTime
|
||||||
|
*/
|
||||||
|
private _isValidTime(workTime: number): boolean {
|
||||||
|
const day = new Date(workTime * 1000).getDay();
|
||||||
|
|
||||||
|
if (this._validDays.indexOf(day) > -1) {
|
||||||
|
const dayBase: number = ~~(workTime / this._dayLength) * this._dayLength;
|
||||||
|
const dayPosition: number = workTime - dayBase;
|
||||||
|
|
||||||
|
return dayPosition >= this._validStart && dayPosition < this._validEnd - 1;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate short term parking costs
|
||||||
|
*/
|
||||||
|
private _calcShortTerm(): number {
|
||||||
|
const startSeconds: number = this._startDT.getTime() / 1000;
|
||||||
|
const endSeconds: number = this._endDT.getTime() / 1000;
|
||||||
|
let workTime: number;
|
||||||
|
|
||||||
|
let validHours = 0;
|
||||||
|
let startHourPart = 0;
|
||||||
|
|
||||||
|
// Calculate Start Hour
|
||||||
|
const startBaseHour: number = ~~(startSeconds / this._hourLength) * this._hourLength;
|
||||||
|
|
||||||
|
// Calculate End Hour
|
||||||
|
const endBaseHour: number = ~~(endSeconds / this._hourLength) * this._hourLength;
|
||||||
|
|
||||||
|
// Check if the very first portion of the hour is valid
|
||||||
|
if (this._isValidTime(startSeconds)) {
|
||||||
|
startHourPart = this._hourLength - (startSeconds - startBaseHour);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go to the next hour after the initial start
|
||||||
|
workTime = startBaseHour + this._hourLength;
|
||||||
|
|
||||||
|
// If there's more of an hours worth of time then loop through the next hours
|
||||||
|
if (endBaseHour - workTime > this._hourLength) {
|
||||||
|
do {
|
||||||
|
validHours = validHours + (this._isValidTime(workTime) ? 1 : 0);
|
||||||
|
workTime = workTime + this._hourLength;
|
||||||
|
} while (workTime <= endBaseHour);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum it all
|
||||||
|
return startHourPart * this._secondPrice + validHours * this._shortPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate long term parking costs
|
||||||
|
* @returns {number}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _calcLongTerm(): number {
|
||||||
|
// convert to time stamps and make them second based
|
||||||
|
const startSeconds: number = this._startDT.getTime() / 1000;
|
||||||
|
const endSeconds: number = this._endDT.getTime() / 1000;
|
||||||
|
|
||||||
|
// Very start of the first day
|
||||||
|
const startBaseSeconds: number = ~~(startSeconds / this._dayLength) * this._dayLength;
|
||||||
|
|
||||||
|
// Very end of the second day
|
||||||
|
const endBaseSeconds: number = ~~(endSeconds / this._dayLength) * this._dayLength + this._dayLength;
|
||||||
|
|
||||||
|
// Subtract the start seconds from the end seconds
|
||||||
|
const distance: number = endBaseSeconds - startBaseSeconds;
|
||||||
|
|
||||||
|
// divide them by number of seconds in a day and multiply by cost
|
||||||
|
|
||||||
|
return (distance / this._dayLength) * this._longPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The public calculate method
|
||||||
|
* @param start
|
||||||
|
* @param end
|
||||||
|
* @param mode
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
calculate(start: string, end: string, mode: string = 'longterm'): number {
|
||||||
|
let workVal = 0.0;
|
||||||
|
this._startDT = parse(start, 'DD/MM/YYYY HH:mm:ssZ');
|
||||||
|
this._endDT = parse(end, 'DD/MM/YYYY HH:mm:ssZ');
|
||||||
|
|
||||||
|
switch (mode.toLowerCase()) {
|
||||||
|
case 'short':
|
||||||
|
case 'shortterm':
|
||||||
|
workVal = this._calcShortTerm();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'long':
|
||||||
|
case 'longterm':
|
||||||
|
workVal = this._calcLongTerm();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseFloat(workVal.toFixed(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Carpark;
|
Loading…
Reference in New Issue
Block a user