diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1a6bd45 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +package-lock.json binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e7ea1e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,117 @@ +## 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/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# 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 +.parcel-cache + +# Next.js build output +.next +out + +# 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 + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.pnp.* diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..e275c1c --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,126 @@ +const path = require("path"); + +module.exports = grunt => { + grunt.initConfig({ + pkg: grunt.file.readJSON("package.json"), + clean: { + default: ["dist/"], + }, + copy: { + html: { + files: [{expand: true, cwd: "src/main/", src: "**/*.html", dest: "dist/"}], + }, + }, + focus: { + dev: { + include: ["html", "js", "link"], + }, + }, + replace: { + dev: { + src: ["./dist/*.html", "./dist/*.js"], + replacements: [ + { + from: "%%VERSION_NUMBER%%", + to: "<%= pkg.version %>+" + new Date().toISOString().slice(0, 19).replace(/[-:T]/g, ""), + } + ], + overwrite: true, + }, + deploy: { + src: ["./dist/*.html", "./dist/*.js"], + replacements: [ + { + from: "%%VERSION_NUMBER%%", + to: "<%= pkg.version %>", + }, + ], + overwrite: true, + }, + }, + watch: { + html: { + files: ["src/main/**/*.html"], + tasks: ["copy:html"], + }, + js: { + files: ["src/main/**/*.js"], + tasks: ["webpack:dev", "replace:dev"], + }, + link: { + files: ["node_modules/@fwdekker/*/dist/**"], + tasks: ["webpack:dev", "replace:dev"], + }, + }, + webpack: { + options: { + entry: "./src/main/js/Main.js", + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + }, + { + test: /\.css$/i, + use: ["style-loader", "css-loader"], + }, + { + test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, + use: [{ + loader: "file-loader", + options: { + name: "[name].[ext]", + outputPath: "./", + }, + }], + }, + ], + }, + resolve: { + extensions: [".css", ".js"], + }, + output: { + filename: "bundle.js", + path: path.resolve(__dirname, "dist/"), + }, + }, + dev: { + mode: "development", + devtool: "inline-source-map", + }, + deploy: { + mode: "production", + }, + }, + }); + + grunt.loadNpmTasks("grunt-contrib-clean"); + grunt.loadNpmTasks("grunt-contrib-copy"); + grunt.loadNpmTasks("grunt-contrib-watch"); + grunt.loadNpmTasks("grunt-focus"); + grunt.loadNpmTasks("grunt-text-replace"); + grunt.loadNpmTasks("grunt-webpack"); + + grunt.registerTask("dev", [ + // Pre + "clean", + // Copy files + "copy:html", + // Compile JS + "webpack:dev", + "replace:dev", + ]); + grunt.registerTask("dev:server", ["dev", "focus:dev"]); + grunt.registerTask("deploy", [ + // Pre + "clean", + // Copy files + "copy:html", + // Compile JS + "webpack:deploy", + "replace:deploy", + ]); + + grunt.registerTask("default", ["dev"]); +}; diff --git a/README.md b/README.md index 3b76aad..ecf5d1c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,22 @@ # Simplify fractions Simplifies a fraction of integers by dividing both operands by their greatest common divisor. + +## Development +### Requirements +* [npm](https://www.npmjs.com/) + +### Setting up +```shell script +# Install dependencies (only needed once) +$> npm ci +``` + +### Building +```shell script +# Build the tool in `dist/` for development +$> npm run dev +# Same as above, but automatically rerun it whenever files are changed +$> npm run dev:server +# Build the tool in `dist/` for deployment +$> npm run deploy +``` diff --git a/index.html b/index.html deleted file mode 100644 index 60d49ab..0000000 --- a/index.html +++ /dev/null @@ -1,226 +0,0 @@ - - - - - - - - - - - Simplify fractions | FWDekker - - - - - -
- -
-
-

Simplify fractions

- -
-

Simplify a fraction to eliminate common factors.

-
-
-
- - - -
-
-
- - - - - -
-
-
- - - -
- -
- - - - -
- - - - - - - - diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..44cfd99 Binary files /dev/null and b/package-lock.json differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..c2bcaf1 --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "simplify-fractions", + "version": "v1.2.0", + "description": "Simple web tool for simplifying fractions", + "author": "Felix W. Dekker", + "browser": "dist/bundle.js", + "repository": { + "type": "git", + "url": "git@git.fwdekker.com:FWDekker/simplify-fractions.git" + }, + "private": true, + "scripts": { + "clean": "grunt clean", + "dev": "grunt dev", + "dev:server": "grunt dev:server", + "deploy": "grunt deploy" + }, + "dependencies": { + "@fwdekker/template": "^0.0.22", + "katex": "^0.13.0" + }, + "devDependencies": { + "css-loader": "^5.2.0", + "file-loader": "^6.2.0", + "grunt": "^1.3.0", + "grunt-cli": "^1.4.1", + "grunt-contrib-clean": "^2.0.0", + "grunt-contrib-copy": "^1.0.0", + "grunt-contrib-watch": "^1.1.0", + "grunt-focus": "^1.0.0", + "grunt-text-replace": "^0.4.0", + "grunt-webpack": "^4.0.2", + "style-loader": "^2.0.0", + "webpack": "^5.28.0", + "webpack-cli": "^4.5.0" + } +} diff --git a/src/main/index.html b/src/main/index.html new file mode 100644 index 0000000..dceca9c --- /dev/null +++ b/src/main/index.html @@ -0,0 +1,52 @@ + + + + + + + + + + + Simplify fractions | FWDekker + + + + + +
+ +
+ + + +
+
+
+ + + + + +
+
+
+ + +
+ +
+
+ +
+ + + + diff --git a/src/main/js/Main.js b/src/main/js/Main.js new file mode 100644 index 0000000..39e0180 --- /dev/null +++ b/src/main/js/Main.js @@ -0,0 +1,174 @@ +import {$, doAfterLoad, footer, header, nav, showPage} from "@fwdekker/template"; +import katex from "katex"; +import "katex/dist/katex.min.css"; + + +/** + * A fraction that can be simplified. + */ +class Fraction { + constructor(numerator, denominator) { + if (!isInt(numerator) || !isInt(denominator)) + throw new Error("Numerator and denominator must be integer-like."); + + this.sign = numerator < 0 !== denominator < 0 ? -1 : 1; + this.numerator = Math.abs(+numerator); + this.denominator = Math.abs(+denominator); + } + + + /** + * Returns a new fraction such that the gcd of the numerator and denominator is 1. + * + * @returns {Fraction} a new fraction such that the gcd of the numerator and denominator is 1 + */ + simplify() { + const common = gcd(this.numerator, this.denominator); + return new Fraction(this.sign * this.numerator / common, this.denominator / common); + } + + /** + * Returns the LaTeX string representation of this fraction. + * + * @returns {string} the LaTeX string representation of this fraction + */ + toString() { + let frac = `\\frac{${this.numerator}}{${this.denominator}}`; + if (this.sign === -1) + frac = `-${frac}`; + + return frac; + } + + /** + * Returns the LaTeX string representation of this fraction, or of the numerator if the denominator is 1. + * + * @returns {string} the LaTeX string representation of this fraction, or of the numerator if the denominator + * is 1. + */ + toReducedString() { + if (this.numerator === 0) + return "0"; + + let frac; + if (this.denominator === 1) + frac = `${this.numerator}`; + else + frac = `\\frac{${this.numerator}}{${this.denominator}}`; + + if (this.sign === -1) + frac = `-${frac}`; + + return frac; + } +} + + +// noinspection EqualityComparisonWithCoercionJS +/** + * Returns `true` if and only if `n` is an integer. + * + * @param n {*} the value to check for integerness + * @returns {boolean} `true` if and only if `n` is an integer + */ +const isInt = n => n == parseInt(n); + +/** + * Returns the greatest common divisor of `a` and `b`. + * + * @param a {number} the first operand + * @param b {number} the second operand + * @returns {number} the greatest common divisor of `a` and `b` + */ +const gcd = (a, b) => { + if (b > a) { + const temp = a; + a = b; + b = temp; + } + + while (true) { + if (b === 0) return a; + a %= b; + + if (a === 0) return b; + b %= a; + } +}; + + +doAfterLoad(() => { + $("#nav").appendChild(nav("/Tools/Simplify Fractions/")); + $("#header").appendChild(header({ + title: "Simplify Fractions", + description: "Simple web tool for simplifying fractions" + })); + $("#footer").appendChild(footer({ + author: "Felix W. Dekker", + authorURL: "https://fwdekker.com/", + license: "MIT License", + licenseURL: "https://git.fwdekker.com/FWDekker/simplify-fractions/src/branch/master/LICENSE", + vcs: "git", + vcsURL: "https://git.fwdekker.com/FWDekker/simplify-fractions/", + version: "v%%VERSION_NUMBER%%" + })); + showPage(); +}); + +doAfterLoad(() => { + const numeratorInput = $("#numerator"); + const denominatorInput = $("#denominator"); + const outputField = $("#out"); + + + /** + * Returns `undefined` if the inputs are valid, or a tuple consisting of the invalid element and an explanation + * of its invalidity otherwise. + * + * @param numerator {string} the numerator value + * @param denominator {string} the denominator value + * @returns {(HTMLElement|string)[]|undefined} `undefined` if the inputs are valid, or a tuple consisting of the + * invalid element and an explanation of its invalidity otherwise + */ + const validateInputs = (numerator, denominator) => { + if (numerator === "") + return [numeratorInput, ""]; + if (denominator === "") + return [denominatorInput, ""]; + if (!isInt(numerator)) + return [numeratorInput, "Numerator must be an integer."]; + if (!isInt(denominator)) + return [denominatorInput, "Denominator must be an integer."]; + if (+denominator === 0) + return [denominatorInput, "Denominator must not be 0."]; + + return undefined; + }; + + /** + * Reads the inputs and tries to output the simplified fraction. + */ + const outputSimplifiedFraction = () => { + let numerator = numeratorInput.value; + let denominator = denominatorInput.value; + + const validationInfo = validateInputs(numerator, denominator); + if (validationInfo !== undefined) { + outputField.innerText = validationInfo[1]; + return; + } + + const fraction = new Fraction(numeratorInput.value, denominatorInput.value); + outputField.innerHTML = katex.renderToString( + fraction.toString() + " = " + fraction.simplify().toReducedString(), + { + displayMode: true, + throwOnError: false + } + ); + }; + + + numeratorInput.addEventListener("input", () => outputSimplifiedFraction()); + denominatorInput.addEventListener("input", () => outputSimplifiedFraction()); +});