├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .travis.yml ├── index.js ├── license ├── package.json ├── readme.md └── test ├── .jscsrc ├── .jshintrc └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | /coverage/ 3 | /node_modules/ 4 | /.idea/ 5 | /sandbox.* 6 | /sandbox/ 7 | /build/ 8 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset" : "google", 3 | "fileExtensions": [".js"], 4 | "validateQuoteMarks" : { 5 | "mark": "'", 6 | "escape": true 7 | }, 8 | "excludeFiles" : ["node_modules/**", "test/**", "coverage/**", "dist/**", "build/**", "sandbox.js", "sandbox/**"] 9 | } 10 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "curly": true, 6 | "immed": true, 7 | "newcap": true, 8 | "noarg": true, 9 | "undef": true, 10 | "unused": "vars", 11 | "strict": true 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | 4 | node_js: 5 | - 'iojs' 6 | - '0.12' 7 | - '0.10' 8 | 9 | after_script: 10 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 11 | - cat ./coverage/lcov.info | ./node_modules/codeclimate-test-reporter/bin/codeclimate.js 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | wrap: wrapRange, 4 | limit: limitRange, 5 | validate: validateRange, 6 | test: testRange, 7 | curry: curry, 8 | name: name 9 | }; 10 | 11 | function wrapRange(min, max, value) { 12 | var maxLessMin = max - min; 13 | return ((value - min) % maxLessMin + maxLessMin) % maxLessMin + min; 14 | } 15 | 16 | function limitRange(min, max, value) { 17 | return Math.max(min, Math.min(max, value)); 18 | } 19 | 20 | function validateRange(min, max, value, minExclusive, maxExclusive) { 21 | if (!testRange(min, max, value, minExclusive, maxExclusive)) { 22 | throw new Error(value + ' is outside of range [' + min + ',' + max + ')'); 23 | } 24 | return value; 25 | } 26 | 27 | function testRange(min, max, value, minExclusive, maxExclusive) { 28 | return !( 29 | value < min || 30 | value > max || 31 | (maxExclusive && (value === max)) || 32 | (minExclusive && (value === min)) 33 | ); 34 | } 35 | 36 | function name(min, max, minExcl, maxExcl) { 37 | return (minExcl ? '(' : '[') + min + ',' + max + (maxExcl ? ')' : ']'); 38 | } 39 | 40 | function curry(min, max, minExclusive, maxExclusive) { 41 | var boundNameFn = name.bind(null, min, max, minExclusive, maxExclusive); 42 | return { 43 | wrap: wrapRange.bind(null, min, max), 44 | limit: limitRange.bind(null, min, max), 45 | validate: function(value) { 46 | return validateRange(min, max, value, minExclusive, maxExclusive); 47 | }, 48 | test: function(value) { 49 | return testRange(min, max, value, minExclusive, maxExclusive); 50 | }, 51 | toString: boundNameFn, 52 | name: boundNameFn 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 James Talmage (github.com/jamestalmage) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "normalize-range", 3 | "version": "0.1.2", 4 | "description": "Utility for normalizing a numeric range, with a wrapping function useful for polar coordinates", 5 | "license": "MIT", 6 | "repository": "jamestalmage/normalize-range", 7 | "author": { 8 | "name": "James Talmage", 9 | "email": "james@talmage.io", 10 | "url": "github.com/jamestalmage" 11 | }, 12 | "engines": { 13 | "node": ">=0.10.0" 14 | }, 15 | "scripts": { 16 | "test": "npm run cover && npm run lint && npm run style", 17 | "cover": "istanbul cover ./node_modules/.bin/_mocha", 18 | "lint": "jshint --reporter=node_modules/jshint-stylish *.js test/*.js", 19 | "debug": "mocha", 20 | "watch": "mocha -w", 21 | "style": "jscs *.js ./**/*.js && jscs ./test/** --config=./test/.jscsrc" 22 | }, 23 | "files": [ 24 | "index.js" 25 | ], 26 | "keywords": [ 27 | "range", 28 | "normalize", 29 | "utility", 30 | "angle", 31 | "degrees", 32 | "polar" 33 | ], 34 | "dependencies": {}, 35 | "devDependencies": { 36 | "almost-equal": "^1.0.0", 37 | "codeclimate-test-reporter": "^0.1.0", 38 | "coveralls": "^2.11.2", 39 | "istanbul": "^0.3.17", 40 | "jscs": "^2.1.1", 41 | "jshint": "^2.8.0", 42 | "jshint-stylish": "^2.0.1", 43 | "mocha": "^2.2.5", 44 | "stringify-pi": "0.0.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # normalize-range 2 | 3 | Utility for normalizing a numeric range, with a wrapping function useful for polar coordinates. 4 | 5 | [![Build Status](https://travis-ci.org/jamestalmage/normalize-range.svg?branch=master)](https://travis-ci.org/jamestalmage/normalize-range) 6 | [![Coverage Status](https://coveralls.io/repos/jamestalmage/normalize-range/badge.svg?branch=master&service=github)](https://coveralls.io/github/jamestalmage/normalize-range?branch=master) 7 | [![Code Climate](https://codeclimate.com/github/jamestalmage/normalize-range/badges/gpa.svg)](https://codeclimate.com/github/jamestalmage/normalize-range) 8 | [![Dependency Status](https://david-dm.org/jamestalmage/normalize-range.svg)](https://david-dm.org/jamestalmage/normalize-range) 9 | [![devDependency Status](https://david-dm.org/jamestalmage/normalize-range/dev-status.svg)](https://david-dm.org/jamestalmage/normalize-range#info=devDependencies) 10 | 11 | [![NPM](https://nodei.co/npm/normalize-range.png)](https://nodei.co/npm/normalize-range/) 12 | 13 | ## Usage 14 | 15 | ```js 16 | var nr = require('normalize-range'); 17 | 18 | nr.wrap(0, 360, 400); 19 | //=> 40 20 | 21 | nr.wrap(0, 360, -90); 22 | //=> 270 23 | 24 | nr.limit(0, 100, 500); 25 | //=> 100 26 | 27 | nr.limit(0, 100, -20); 28 | //=> 0 29 | 30 | // There is a convenient currying function 31 | var wrapAngle = nr.curry(0, 360).wrap; 32 | var limitTo10 = nr.curry(0, 10).limit; 33 | 34 | wrapAngle(-30); 35 | //=> 330 36 | ``` 37 | ## API 38 | 39 | ### wrap(min, max, value) 40 | 41 | Normalizes a values that "wraps around". For example, in a polar coordinate system, 270˚ can also be 42 | represented as -90˚. 43 | For wrapping purposes we assume `max` is functionally equivalent to `min`, and that `wrap(max + 1) === wrap(min + 1)`. 44 | Wrap always assumes that `min` is *inclusive*, and `max` is *exclusive*. 45 | In other words, if `value === max` the function will wrap it, and return `min`, but `min` will not be wrapped. 46 | 47 | ```js 48 | nr.wrap(0, 360, 0) === 0; 49 | nr.wrap(0, 360, 360) === 0; 50 | nr.wrap(0, 360, 361) === 1; 51 | nr.wrap(0, 360, -1) === 359; 52 | ``` 53 | 54 | You are not restricted to whole numbers, and ranges can be negative. 55 | 56 | ```js 57 | var π = Math.PI; 58 | var radianRange = nr.curry(-π, π); 59 | 60 | redianRange.wrap(0) === 0; 61 | nr.wrap(π) === -π; 62 | nr.wrap(4 * π / 3) === -2 * π / 3; 63 | ``` 64 | 65 | ### limit(min, max, value) 66 | 67 | Normalize the value by bringing it within the range. 68 | If `value` is greater than `max`, `max` will be returned. 69 | If `value` is less than `min`, `min` will be returned. 70 | Otherwise, `value` is returned unaltered. 71 | Both ends of this range are *inclusive*. 72 | 73 | ### test(min, max, value, [minExclusive], [maxExclusive]) 74 | 75 | Returns `true` if `value` is within the range, `false` otherwise. 76 | It defaults to `inclusive` on both ends of the range, but that can be 77 | changed by setting `minExclusive` and/or `maxExclusive` to a truthy value. 78 | 79 | ### validate(min, max, value, [minExclusive], [maxExclusive]) 80 | 81 | Returns `value` or throws an error if `value` is outside the specified range. 82 | 83 | ### name(min, max, value, [minExclusive], [maxExclusive]) 84 | 85 | Returns a string representing this range in 86 | [range notation](https://en.wikipedia.org/wiki/Interval_(mathematics)#Classification_of_intervals). 87 | 88 | ### curry(min, max, [minExclusive], [maxExclusive]) 89 | 90 | Convenience method for currying all method arguments except `value`. 91 | 92 | ```js 93 | var angle = require('normalize-range').curry(-180, 180, false, true); 94 | 95 | angle.wrap(270) 96 | //=> -90 97 | 98 | angle.limit(200) 99 | //=> 180 100 | 101 | angle.test(0) 102 | //=> true 103 | 104 | angle.validate(300) 105 | //=> throws an Error 106 | 107 | angle.toString() // or angle.name() 108 | //=> "[-180,180)" 109 | ``` 110 | 111 | #### min 112 | 113 | *Required* 114 | Type: `number` 115 | 116 | The minimum value (inclusive) of the range. 117 | 118 | #### max 119 | 120 | *Required* 121 | Type: `number` 122 | 123 | The maximum value (exclusive) of the range. 124 | 125 | #### value 126 | 127 | *Required* 128 | Type: `number` 129 | 130 | The value to be normalized. 131 | 132 | #### returns 133 | 134 | Type: `number` 135 | 136 | The normalized value. 137 | 138 | ## Building and Releasing 139 | 140 | - `npm test`: tests, linting, coverage and style checks. 141 | - `npm run watch`: autotest mode for active development. 142 | - `npm run debug`: run tests without coverage (istanbul can obscure line #'s) 143 | 144 | Release via `cut-release` tool. 145 | 146 | ## License 147 | 148 | MIT © [James Talmage](http://github.com/jamestalmage) 149 | -------------------------------------------------------------------------------- /test/.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset" : "google", 3 | "fileExtensions": [".js"], 4 | "validateQuoteMarks" : { 5 | "mark": "'", 6 | "escape": true 7 | }, 8 | "maximumLineLength" : 120 9 | } 10 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.jshintrc", 3 | "mocha": true 4 | } 5 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var assert = require('assert'); 3 | var ranges = require('../'); 4 | var namePi = require('stringify-pi'); 5 | var almostEqual = require('almost-equal'); 6 | 7 | var π = Math.PI; 8 | 9 | 10 | function close(a, b) { 11 | return almostEqual(a, b, almostEqual.DBL_EPSILON, almostEqual.DBL_EPSILON); 12 | } 13 | 14 | function names(type, min, max, value, expected, minExcl, maxExcl) { 15 | var lb = minExcl ? '(' : '['; 16 | var rb = maxExcl ? ')' : ']'; 17 | var rangeName = type + ': ' + lb + namePi(min) + ',' + namePi(max) + rb; 18 | var testName = rangeName + ' ' + namePi(value) + ' === ' + namePi(expected); 19 | 20 | return { 21 | range: rangeName, 22 | test: testName 23 | }; 24 | } 25 | 26 | function _test(type, min, max, value, expected, minExcl, maxExcl) { 27 | var n = names(type, min, max, value, expected, minExcl, maxExcl); 28 | it(n.test, function() { 29 | var result = ranges[type](min, max, value, minExcl, maxExcl); 30 | if (!close(result, expected)) { 31 | assert.strictEqual( 32 | result, 33 | expected, 34 | n.range 35 | ); 36 | } 37 | }); 38 | } 39 | 40 | describe('wrap ', function() { 41 | function test(min, max, value, expected) { 42 | return _test('wrap', min, max, value, expected, false, true); 43 | } 44 | 45 | var angleWrap = ranges.curry(0, 360).wrap; 46 | function angle(value, expected) { 47 | test(0, 360, value, expected); 48 | 49 | var message = 'angleWrap(' + value + ') === ' + expected; 50 | it(message, function() { 51 | assert.strictEqual( 52 | angleWrap(value), 53 | expected, 54 | message 55 | ); 56 | }); 57 | } 58 | 59 | function angle2(value, expected) { 60 | test(-180, 180, value, expected); 61 | } 62 | 63 | angle(-270, 90); 64 | angle(-20, 340); 65 | angle(0, 0); 66 | angle(20, 20); 67 | angle(100, 100); 68 | angle(352.5, 352.5); 69 | angle(360, 0); 70 | angle(400, 40); 71 | angle(720, 0); 72 | 73 | angle2(-200, 160); 74 | angle2(-181, 179); 75 | angle2(-180, -180); 76 | angle2(-20, -20); 77 | angle2(0, 0); 78 | angle2(0, 0); 79 | 80 | test(-π, π, -π, -π); 81 | test(-π, π, 0, 0); 82 | test(-π, π, 2 * π, 0); 83 | test(-π, π, 2 * π / 3, 2 * π / 3); 84 | test(-π, π, 4 * π / 3, -2 * π / 3); 85 | }); 86 | 87 | describe('limit', function() { 88 | var test = _test.bind(null, 'limit'); 89 | 90 | test(0, 10, -1, 0); 91 | test(0, 10, 0, 0); 92 | test(0, 10, 5, 5); 93 | test(0, 10, 10, 10); 94 | test(0, 10, 11, 10); 95 | }); 96 | 97 | describe('test', function() { 98 | function isOk(min, max, value, minExcl, maxExcl, ok) { 99 | var n = names('test', min, max, value, ok ? 'ok' : 'notOk', minExcl, maxExcl); 100 | 101 | it(n.test, function() { 102 | assert.strictEqual( 103 | !!ok, 104 | ranges.test(min, max, value, minExcl, maxExcl), 105 | n.test 106 | ); 107 | }); 108 | } 109 | 110 | function ok(min, max, value, minExcl, maxExcl) { 111 | isOk(min, max, value, minExcl, maxExcl, true); 112 | } 113 | 114 | function notOk(min, max, value, minExcl, maxExcl) { 115 | isOk(min, max, value, minExcl, maxExcl, false); 116 | } 117 | 118 | notOk(0, 10, -1); 119 | notOk(0, 10, 0, true); 120 | ok(0, 10, 0); 121 | ok(0, 10, 9); 122 | ok(0, 10, 10); 123 | notOk(0, 10, 10, false, true); 124 | notOk(0, 10, 11); 125 | 126 | it('curried', function() { 127 | var c = ranges.curry(0, 10, false, true).test; 128 | assert.strictEqual(c(0), true); 129 | assert.strictEqual(c(10), false); 130 | assert.strictEqual(c(8), true); 131 | assert.strictEqual(c(11), false); 132 | }); 133 | }); 134 | 135 | describe('validate', function() { 136 | 137 | function isOk(min, max, value, minExcl, maxExcl, ok) { 138 | var n = names('test', min, max, value, ok ? 'ok' : 'notOk', minExcl, maxExcl); 139 | 140 | it(n.test, function() { 141 | if (ok) { 142 | assert.strictEqual( 143 | ranges.validate(min, max, value, minExcl, maxExcl), 144 | value, 145 | n.test 146 | ); 147 | } else { 148 | assert.throws(function() { 149 | ranges.validate(min, max, value, minExcl, maxExcl); 150 | }, null, n.test); 151 | } 152 | 153 | }); 154 | } 155 | 156 | function ok(min, max, value, minExcl, maxExcl) { 157 | isOk(min, max, value, minExcl, maxExcl, true); 158 | } 159 | 160 | function notOk(min, max, value, minExcl, maxExcl) { 161 | isOk(min, max, value, minExcl, maxExcl, false); 162 | } 163 | 164 | notOk(0, 10, -1); 165 | notOk(0, 10, 0, true); 166 | ok(0, 10, 0); 167 | ok(0, 10, 9); 168 | ok(0, 10, 10); 169 | notOk(0, 10, 10, false, true); 170 | notOk(0, 10, 11); 171 | 172 | it('curried', function() { 173 | var c = ranges.curry(0, 10, true, false).validate; 174 | assert.throws(function() { 175 | c(0); 176 | }); 177 | assert.strictEqual(c(10), 10); 178 | assert.strictEqual(c(8), 8); 179 | assert.throws(function() { 180 | c(11); 181 | }); 182 | }); 183 | }); 184 | 185 | describe('name', function() { 186 | function test(expected, varargs) { 187 | var args = Array.prototype.slice.call(arguments, 1); 188 | it(expected, function() { 189 | assert.strictEqual( 190 | ranges.name.apply(ranges, args), 191 | expected 192 | ); 193 | }); 194 | } 195 | 196 | test('[0,10]', 0, 10); 197 | test('[6,7]', 6, 7); 198 | test('(-10,7]', -10, 7, true); 199 | test('[-50,13)', -50, 13, false, true); 200 | test('(-1,1)', -1, 1, true, true); 201 | 202 | it('toString() on curried objs', function() { 203 | assert.strictEqual( 204 | '' + ranges.curry(1, 3, true), 205 | '(1,3]' 206 | ); 207 | }); 208 | }); 209 | --------------------------------------------------------------------------------