commit e3b91a9d473b966b2ff4774bce5ff544a9a59c72 Author: Tony Garnock-Jones Date: Sun Sep 9 18:12:50 2018 +0100 Step zero of work toward imperative-syndicate/js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..911168a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +scratch/ +node_modules/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5f73abd --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +.PHONY: all clean veryclean test + +all: + npm install . + +clean: + rm -f dist/*.js + +veryclean: clean + rm -rf node_modules/ + +test: + npm test diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..86cf28d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,166 @@ +{ + "name": "syndicate-ijs", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "commander": { + "version": "2.3.0", + "resolved": "http://registry.npmjs.org/commander/-/commander-2.3.0.tgz", + "integrity": "sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM=", + "dev": true + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "diff": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", + "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz", + "integrity": "sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE=", + "dev": true + }, + "expect.js": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz", + "integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=", + "dev": true + }, + "glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true, + "requires": { + "inherits": "2", + "minimatch": "0.3" + } + }, + "growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=", + "dev": true + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "jade": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", + "dev": true, + "requires": { + "commander": "0.6.1", + "mkdirp": "0.3.0" + }, + "dependencies": { + "commander": { + "version": "0.6.1", + "resolved": "http://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", + "dev": true + }, + "mkdirp": { + "version": "0.3.0", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", + "dev": true + } + } + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "2.5.3", + "resolved": "http://registry.npmjs.org/mocha/-/mocha-2.5.3.tgz", + "integrity": "sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg=", + "dev": true, + "requires": { + "commander": "2.3.0", + "debug": "2.2.0", + "diff": "1.4.0", + "escape-string-regexp": "1.0.2", + "glob": "3.2.11", + "growl": "1.9.2", + "jade": "0.26.3", + "mkdirp": "0.5.1", + "supports-color": "1.2.0", + "to-iso-string": "0.0.2" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "supports-color": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz", + "integrity": "sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4=", + "dev": true + }, + "to-iso-string": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/to-iso-string/-/to-iso-string-0.0.2.tgz", + "integrity": "sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE=", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f44877c --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "syndicate-ijs", + "version": "0.0.0", + "description": "Imperative Syndicate in the browser", + "homepage": "https://github.com/tonyg/syndicate", + "main": "src/main.js", + "repository": { + "type": "git", + "url": "git://github.com/tonyg/syndicate" + }, + "directories": { + "bin": "./bin" + }, + "scripts": { + "clean": "rm -f dist/*", + "test": "mocha" + }, + "author": "Tony Garnock-Jones ", + "devDependencies": { + "expect.js": "^0.3.1", + "immutable": "^3.8.2", + "mocha": "^2.5.3" + } +} diff --git a/src/bag.js b/src/bag.js new file mode 100644 index 0000000..50f5ae2 --- /dev/null +++ b/src/bag.js @@ -0,0 +1,101 @@ +"use strict"; +// Bags and Deltas (which are Bags where item-counts can be negative). + +const Immutable = require("immutable"); + +const PRESENT_TO_ABSENT = -1; +const ABSENT_TO_ABSENT = 0; +const ABSENT_TO_PRESENT = 1; +const PRESENT_TO_PRESENT = 2; + +/////////////////////////////////////////////////////////////////////////// + +function MutableBag(s) { + this._items = s ? fromSet(s) : Immutable.Map(); +} + +MutableBag.prototype.change = function (key, delta) { + var net; + ({bag: this._items, net: net} = change(this._items, key, delta)); + return net; +}; + +MutableBag.prototype.get = function (key) { + return get(this._items, key); +}; + +MutableBag.prototype.clear = function () { + this._items = Immutable.Map(); +}; + +MutableBag.prototype.includes = function (key) { + return includes(this._items, key); +}; + +MutableBag.prototype.isEmpty = function () { + return this._items.isEmpty(); +}; + +MutableBag.prototype.count = function () { + return this._items.count(); +}; + +MutableBag.prototype.keys = function () { + return this._items.keys(); +}; + +MutableBag.prototype.entries = function () { + return this._items.entries(); +}; + +/////////////////////////////////////////////////////////////////////////// + +const Bag = Immutable.Map; + +function fromSet(s) { + return Bag().withMutations(function (b) { + for (let v of Immutable.Set(s)) { + b = b.set(v, 1); + } + }); +} + +function change(bag, key, delta, clamp) { + let oldCount = get(bag, key); + let newCount = oldCount + delta; + if (clamp) { + newCount = Math.max(0, newCount); + } + if (newCount === 0) { + return { + bag: bag.remove(key), + net: (oldCount === 0) ? ABSENT_TO_ABSENT : PRESENT_TO_ABSENT + }; + } else { + return { + bag: bag.set(key, newCount), + net: (oldCount === 0) ? ABSENT_TO_PRESENT : PRESENT_TO_PRESENT + }; + } +} + +function get(bag, key) { + return bag.get(key, 0); +} + +function includes(bag, key) { + return get(bag, key) > 0; +} + +/////////////////////////////////////////////////////////////////////////// + +module.exports.PRESENT_TO_ABSENT = PRESENT_TO_ABSENT; +module.exports.ABSENT_TO_ABSENT = ABSENT_TO_ABSENT; +module.exports.ABSENT_TO_PRESENT = ABSENT_TO_PRESENT; +module.exports.PRESENT_TO_PRESENT = PRESENT_TO_PRESENT; +module.exports.MutableBag = MutableBag; +module.exports.Bag = Bag; +module.exports.fromSet = fromSet; +module.exports.change = change; +module.exports.get = get; +module.exports.includes = includes; diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..f1152ef --- /dev/null +++ b/src/main.js @@ -0,0 +1,4 @@ +"use strict"; + +module.exports.Bag = require("./bag.js"); +// module.exports.Skeleton = require("./skeleton.js"); diff --git a/test/test-bag.js b/test/test-bag.js new file mode 100644 index 0000000..e59cffa --- /dev/null +++ b/test/test-bag.js @@ -0,0 +1,114 @@ +"use strict"; + +var expect = require('expect.js'); +var Immutable = require('immutable'); +var Bag = require('../src/bag.js'); + +describe('immutable bag', function () { + it('should be initializable from a set', function () { + var b = Bag.fromSet(Immutable.Set(['a', 'b', 'c'])); + expect(b.count()).to.equal(3); + expect(Bag.get(b, 'a')).to.equal(1); + expect(Bag.get(b, 'z')).to.equal(0); + }); + + it('should be initializable from an array', function () { + var b = Bag.fromSet(['a', 'b', 'c', 'a']); + expect(b.count()).to.equal(3); + expect(Bag.get(b, 'a')).to.equal(1); + expect(Bag.get(b, 'z')).to.equal(0); + }); + + it('should be immutable', function () { + var b = Bag.Bag(); + Bag.change(b, 'a', 1); + Bag.change(b, 'a', 1); + expect(b).to.equal(Bag.Bag()); + }); + + it('should count up', function () { + var b = Bag.Bag(); + var change1, change2; + ({bag: b, net: change1} = Bag.change(b, 'a', 1)); + ({bag: b, net: change2} = Bag.change(b, 'a', 1)); + expect(change1).to.equal(Bag.ABSENT_TO_PRESENT); + expect(change2).to.equal(Bag.PRESENT_TO_PRESENT); + expect(Bag.get(b, 'a')).to.equal(2); + expect(Bag.get(b, 'z')).to.equal(0); + }); + + it('should count down', function () { + var b = Bag.fromSet(['a']); + var c1, c2, c3, c4; + ({bag: b, net: c1} = Bag.change(b, 'a', 1)); + ({bag: b, net: c2} = Bag.change(b, 'a', -1)); + expect(b.count()).to.equal(1); + expect(c1).to.equal(Bag.PRESENT_TO_PRESENT); + expect(c2).to.equal(Bag.PRESENT_TO_PRESENT); + ({bag: b, net: c3} = Bag.change(b, 'a', -1)); + expect(b.count()).to.equal(0); + expect(c3).to.equal(Bag.PRESENT_TO_ABSENT); + expect(Bag.get(b, 'a')).to.equal(0); + expect(Bag.get(b, 'z')).to.equal(0); + ({bag: b, net: c4} = Bag.change(b, 'a', -1)); + expect(b.count()).to.equal(1); + expect(c4).to.equal(Bag.ABSENT_TO_PRESENT); + expect(Bag.get(b, 'a')).to.equal(-1); + }); + + it('should be clamped', function() { + var b = Bag.fromSet(['a']); + ({bag: b} = Bag.change(b, 'a', -1, true)); + ({bag: b} = Bag.change(b, 'a', -1, true)); + ({bag: b} = Bag.change(b, 'a', -1, true)); + ({bag: b} = Bag.change(b, 'a', -1, true)); + expect(b.count()).to.equal(0); + expect(Bag.get(b, 'a')).to.equal(0); + }); +}); + +describe('mutable bag', function () { + it('should be initializable from a set', function () { + var b = new Bag.MutableBag(Immutable.Set(['a', 'b', 'c'])); + expect(b.count()).to.equal(3); + expect(b.get('a')).to.equal(1); + expect(b.get('z')).to.equal(0); + }); + + it('should be initializable from an array', function () { + var b = new Bag.MutableBag(['a', 'b', 'c', 'a']); + expect(b.count()).to.equal(3); + expect(b.get('a')).to.equal(1); + expect(b.get('z')).to.equal(0); + }); + + it('should be mutable', function () { + var b = new Bag.MutableBag(); + b.change('a', 1); + b.change('a', 1); + expect(b.get('a')).to.equal(2); + expect(b.get('z')).to.equal(0); + }); + + it('should count up', function () { + var b = new Bag.MutableBag(); + expect(b.change('a', 1)).to.equal(Bag.ABSENT_TO_PRESENT); + expect(b.change('a', 1)).to.equal(Bag.PRESENT_TO_PRESENT); + expect(b.get('a')).to.equal(2); + expect(b.get('z')).to.equal(0); + }); + + it('should count down', function () { + var b = new Bag.MutableBag(['a']); + expect(b.change('a', 1)).to.equal(Bag.PRESENT_TO_PRESENT); + expect(b.change('a', -1)).to.equal(Bag.PRESENT_TO_PRESENT); + expect(b.count()).to.equal(1); + expect(b.change('a', -1)).to.equal(Bag.PRESENT_TO_ABSENT); + expect(b.count()).to.equal(0); + expect(b.get('a')).to.equal(0); + expect(b.get('z')).to.equal(0); + expect(b.change('a', -1)).to.equal(Bag.ABSENT_TO_PRESENT); + expect(b.count()).to.equal(1); + expect(b.get('a')).to.equal(-1); + }); +});