This commit is contained in:
Martin Donnelly 2020-11-03 23:00:17 +00:00
commit 83007b6f04
11 changed files with 5112 additions and 0 deletions

32
.editorconfig Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
{
"semi": true,
"trailingComma": "none",
"singleQuote": true,
"printWidth": 120
}

0
README.md Normal file
View File

166
lib/carpark.js Normal file
View 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

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View 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
View 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
View 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
View 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;