Repository: rtfeldman/seamless-immutable
Pull request #69 (2015-10-17 05:05:57)

Issue#64 Add set and setIn operations (based on existing merge)
Add simple implementation of set and setIn by constructing a path and invoking merge, which already does all the hard stuff, checking, managing mutability, &c.


Patch:
diff --git a/.gitignore b/.gitignore
index b2ff489..92aef6b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,4 @@ tmp
 html-report
 lcov.info
 lcov-report
+.idea
diff --git a/README.md b/README.md
index 4aa3b61..7cfcdb7 100644
--- a/README.md
+++ b/README.md
@@ -174,6 +174,29 @@ invocations will be performed using each provided object in turn.
 
 A second argument can be provided to perform a deep merge: `{deep: true}`.
 
+### set
+
+```javascript
+Immutable({type: "parrot", subtype: "Norwegian Blue", status: "alive"}).set("status", "dead")
+// returns Immutable({type: "parrot", subtype: "Norwegian Blue", status: "dead"})
+```
+
+Returns an Immutable Object with a single property set to the provided value.
+Basically a more straightforward way of saying
+```javascript
+Immutable({type: "parrot", subtype: "Norwegian Blue", status: "alive"}).merge({status: "dead"})
+```
+(and more convenient with non-literal keys unless you have ES6 ```[computed_property_names]```).
+
+### setIn
+
+Like [set](#set), but accepts a nested path to the property. 
+
+```javascript
+Immutable({type: {main: "parrot", sub: "Norwegian Blue"}, status: "alive"}).setIn(["type", "sub"], "Norwegian Ridgeback")
+// returns Immutable({type: {main: "parrot", sub: "Norwegian Ridgeback"}, status: "alive"})
+```
+
 ### without
 
 ```javascript
diff --git a/benchmark.js b/benchmark.js
new file mode 100644
index 0000000..e69de29
diff --git a/seamless-immutable.development.js b/seamless-immutable.development.js
index e5dccc8..c834188 100644
--- a/seamless-immutable.development.js
+++ b/seamless-immutable.development.js
@@ -88,6 +88,36 @@
     });
   }
 
+  function arraySet(idx, value) {
+    if (idx in this && this[idx] === value) {
+      return this;
+    }
+
+    var mutable = asMutableArray.call(this);
+    mutable[idx] = Immutable(value);
+    return makeImmutableArray(mutable);
+  }
+
+  function arraySetIn(pth, value) {
+    var head = pth[0];
+
+    if (pth.length === 1) {
+      return arraySet.call(this, head, value);
+    } else {
+      var tail = pth.slice(1);
+      // this[head] might (validly) be an (immutable) array or object
+      var newValue = this[head].setIn(tail, value);
+
+      if (head in this && this[head] === newValue) {
+        return this;
+      }
+
+      var mutable = asMutableArray.call(this);
+      mutable[head] = newValue;
+      return makeImmutableArray(mutable);
+    }
+  }
+
   function makeImmutableArray(array) {
     // Don't change their implementations, but wrap these functions to make sure
     // they always return an immutable value.
@@ -101,6 +131,8 @@
     addPropertyTo(array, "flatMap",  flatMap);
     addPropertyTo(array, "asObject", asObject);
     addPropertyTo(array, "asMutable", asMutableArray);
+    addPropertyTo(array, "set", arraySet);
+    addPropertyTo(array, "setIn", arraySetIn);
 
     for(var i = 0, length = array.length; i < length; i++) {
       array[i] = Immutable(array[i]);
@@ -322,6 +354,42 @@
     }
   }
 
+  var immutableEmptyObject = Immutable({});
+
+  function objectSetIn(path, value) {
+    var head = path[0];
+    if (path.length === 1) {
+      return objectSet.call(this, head, value);
+    }
+
+    var tail = path.slice(1);
+    var newValue;
+    if (this.hasOwnProperty(head) && this[head] !== undefined) {
+      // Might (validly) be object or array
+      newValue = this[head].setIn(tail, value);
+    } else {
+      newValue = objectSetIn.call(immutableEmptyObject, tail, value);
+    }
+
+    if (this.hasOwnProperty(head) && this[head] === newValue) {
+      return this;
+    }
+
+    var mutable = quickCopy(this, this.instantiateEmptyObject());
+    mutable[head] = newValue;
+    return makeImmutableObject(mutable, this);
+  }
+
+  function objectSet(property, value) {
+    if (this.hasOwnProperty(property) && this[property] === value) {
+      return this;
+    }
+
+    var mutable = quickCopy(this, this.instantiateEmptyObject());
+    mutable[property] = Immutable(value);
+    return makeImmutableObject(mutable, this);
+  }
+
   function asMutableObject(opts) {
     var result = this.instantiateEmptyObject(), key;
 
@@ -357,6 +425,8 @@
     addPropertyTo(obj, "without", without);
     addPropertyTo(obj, "asMutable", asMutableObject);
     addPropertyTo(obj, "instantiateEmptyObject", instantiateEmptyObject);
+    addPropertyTo(obj, "set", objectSet);
+    addPropertyTo(obj, "setIn", objectSetIn);
 
     return makeImmutable(obj, mutatingObjectMethods);
   }
diff --git a/seamless-immutable.production.min.js b/seamless-immutable.production.min.js
index 7e5e2d0..6a1a7c1 100644
--- a/seamless-immutable.production.min.js
+++ b/seamless-immutable.production.min.js
@@ -1,1 +1,1 @@
-!function(){"use strict";function a(a,b,c){Object.defineProperty(a,b,{enumerable:!1,configurable:!1,writable:!1,value:c})}function b(b){a(b,t,!0)}function c(a){return"object"==typeof a?null===a||a.hasOwnProperty(t):!0}function d(a){return!(null===a||"object"!=typeof a||a instanceof Array||a instanceof Date)}function e(a){var b=new Error(a);return b.__proto__=e,b}function f(a,c){b(a);return a}function g(b,c){var d=b[c];a(b,c,function(){return s(d.apply(b,arguments))})}function h(b){for(var c in x)if(x.hasOwnProperty(c)){var d=x[c];g(b,d)}a(b,"flatMap",i),a(b,"asObject",l),a(b,"asMutable",k);for(var e=0,h=b.length;h>e;e++)b[e]=s(b[e]);return f(b,w)}function i(a){if(0===arguments.length)return this;var b,c=[],d=this.length;for(b=0;d>b;b++){var e=a(this[b],b,this);e instanceof Array?c.push.apply(c,e):c.push(e)}return h(c)}function j(a){if(0===arguments.length)return this;a instanceof Array||(a=Array.prototype.slice.call(arguments));var b=this.instantiateEmptyObject();for(var c in this)this.hasOwnProperty(c)&&-1===a.indexOf(c)&&(b[c]=this[c]);return r(b,{instantiateEmptyObject:this.instantiateEmptyObject})}function k(a){var b,c,d=[];if(a&&a.deep)for(b=0,c=this.length;c>b;b++)d.push(m(this[b]));else for(b=0,c=this.length;c>b;b++)d.push(this[b]);return d}function l(a){"function"!=typeof a&&(a=function(a){return a});var b,c={},d=this.length;for(b=0;d>b;b++){var e=a(this[b],b,this),f=e[0],g=e[1];c[f]=g}return r(c)}function m(a){return!a||!a.hasOwnProperty(t)||a instanceof Date?a:a.asMutable({deep:!0})}function n(a,b){for(var c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b}function o(a,b){function c(a,c,f){var g=s(c[f]),j=i&&i(a[f],g,b),k=a[f];if(void 0!==e||void 0!==j||!a.hasOwnProperty(f)||g!==k&&g===g){var l;l=j?j:h&&d(k)&&d(g)?k.merge(g,b):g,(k!==l&&l===l||!a.hasOwnProperty(f))&&(void 0===e&&(e=n(a,a.instantiateEmptyObject())),e[f]=l)}}if(0===arguments.length)return this;if(null===a||"object"!=typeof a)throw new TypeError("Immutable#merge can only be invoked with objects or arrays, not "+JSON.stringify(a));var e,f,g=a instanceof Array,h=b&&b.deep,i=b&&b.merger;if(g)for(var j=0;j<a.length;j++){var k=a[j];for(f in k)k.hasOwnProperty(f)&&c(this,k,f)}else for(f in a)a.hasOwnProperty(f)&&c(this,a,f);return void 0===e?this:r(e,{instantiateEmptyObject:this.instantiateEmptyObject})}function p(a){var b,c=this.instantiateEmptyObject();if(a&&a.deep)for(b in this)this.hasOwnProperty(b)&&(c[b]=m(this[b]));else for(b in this)this.hasOwnProperty(b)&&(c[b]=this[b]);return c}function q(){return{}}function r(b,c){var d=c&&c.instantiateEmptyObject?c.instantiateEmptyObject:q;return a(b,"merge",o),a(b,"without",j),a(b,"asMutable",p),a(b,"instantiateEmptyObject",d),f(b,u)}function s(a,b){if(c(a))return a;if(a instanceof Array)return h(a.slice());if(a instanceof Date)return f(new Date(a.getTime()));var d=b&&b.prototype,e=d&&d!==Object.prototype?function(){return Object.create(d)}:q,g=e();for(var i in a)a.hasOwnProperty(i)&&(g[i]=s(a[i]));return r(g,{instantiateEmptyObject:e})}var t="__immutable_invariants_hold",u=["setPrototypeOf"],v=["keys"],w=u.concat(["push","pop","sort","splice","shift","unshift","reverse"]),x=v.concat(["map","filter","slice","concat","reduce","reduceRight"]);e.prototype=Error.prototype,s.isImmutable=c,s.ImmutableError=e,Object.freeze(s),"object"==typeof module?module.exports=s:"object"==typeof exports?exports.Immutable=s:"object"==typeof window?window.Immutable=s:"object"==typeof global&&(global.Immutable=s)}();
\ No newline at end of file
+!function(){"use strict";function a(a,b,c){Object.defineProperty(a,b,{enumerable:!1,configurable:!1,writable:!1,value:c})}function b(b){a(b,x,!0)}function c(a){return"object"==typeof a?null===a||a.hasOwnProperty(x):!0}function d(a){return!(null===a||"object"!=typeof a||a instanceof Array||a instanceof Date)}function e(a){var b=new Error(a);return b.__proto__=e,b}function f(a,c){b(a);return a}function g(b,c){var d=b[c];a(b,c,function(){return w(d.apply(b,arguments))})}function h(a,b){if(a in this&&this[a]===b)return this;var c=m.call(this);return c[a]=w(b),j(c)}function i(a,b){var c=a[0];if(1===a.length)return h.call(this,c,b);var d=a.slice(1),e=this[c].setIn(d,b);if(c in this&&this[c]===e)return this;var f=m.call(this);return f[c]=e,j(f)}function j(b){for(var c in B)if(B.hasOwnProperty(c)){var d=B[c];g(b,d)}a(b,"flatMap",k),a(b,"asObject",n),a(b,"asMutable",m),a(b,"set",h),a(b,"setIn",i);for(var e=0,j=b.length;j>e;e++)b[e]=w(b[e]);return f(b,A)}function k(a){if(0===arguments.length)return this;var b,c=[],d=this.length;for(b=0;d>b;b++){var e=a(this[b],b,this);e instanceof Array?c.push.apply(c,e):c.push(e)}return j(c)}function l(a){if(0===arguments.length)return this;a instanceof Array||(a=Array.prototype.slice.call(arguments));var b=this.instantiateEmptyObject();for(var c in this)this.hasOwnProperty(c)&&-1===a.indexOf(c)&&(b[c]=this[c]);return v(b,{instantiateEmptyObject:this.instantiateEmptyObject})}function m(a){var b,c,d=[];if(a&&a.deep)for(b=0,c=this.length;c>b;b++)d.push(o(this[b]));else for(b=0,c=this.length;c>b;b++)d.push(this[b]);return d}function n(a){"function"!=typeof a&&(a=function(a){return a});var b,c={},d=this.length;for(b=0;d>b;b++){var e=a(this[b],b,this),f=e[0],g=e[1];c[f]=g}return v(c)}function o(a){return!a||!a.hasOwnProperty(x)||a instanceof Date?a:a.asMutable({deep:!0})}function p(a,b){for(var c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b}function q(a,b){function c(a,c,f){var g=w(c[f]),j=i&&i(a[f],g,b),k=a[f];if(void 0!==e||void 0!==j||!a.hasOwnProperty(f)||g!==k&&g===g){var l;l=j?j:h&&d(k)&&d(g)?k.merge(g,b):g,(k!==l&&l===l||!a.hasOwnProperty(f))&&(void 0===e&&(e=p(a,a.instantiateEmptyObject())),e[f]=l)}}if(0===arguments.length)return this;if(null===a||"object"!=typeof a)throw new TypeError("Immutable#merge can only be invoked with objects or arrays, not "+JSON.stringify(a));var e,f,g=a instanceof Array,h=b&&b.deep,i=b&&b.merger;if(g)for(var j=0;j<a.length;j++){var k=a[j];for(f in k)k.hasOwnProperty(f)&&c(this,k,f)}else for(f in a)a.hasOwnProperty(f)&&c(this,a,f);return void 0===e?this:v(e,{instantiateEmptyObject:this.instantiateEmptyObject})}function r(a,b){var c=a[0];if(1===a.length)return s.call(this,c,b);var d,e=a.slice(1);if(d=this.hasOwnProperty(c)&&void 0!==this[c]?this[c].setIn(e,b):r.call(C,e,b),this.hasOwnProperty(c)&&this[c]===d)return this;var f=p(this,this.instantiateEmptyObject());return f[c]=d,v(f,this)}function s(a,b){if(this.hasOwnProperty(a)&&this[a]===b)return this;var c=p(this,this.instantiateEmptyObject());return c[a]=w(b),v(c,this)}function t(a){var b,c=this.instantiateEmptyObject();if(a&&a.deep)for(b in this)this.hasOwnProperty(b)&&(c[b]=o(this[b]));else for(b in this)this.hasOwnProperty(b)&&(c[b]=this[b]);return c}function u(){return{}}function v(b,c){var d=c&&c.instantiateEmptyObject?c.instantiateEmptyObject:u;return a(b,"merge",q),a(b,"without",l),a(b,"asMutable",t),a(b,"instantiateEmptyObject",d),a(b,"set",s),a(b,"setIn",r),f(b,y)}function w(a,b){if(c(a))return a;if(a instanceof Array)return j(a.slice());if(a instanceof Date)return f(new Date(a.getTime()));var d=b&&b.prototype,e=d&&d!==Object.prototype?function(){return Object.create(d)}:u,g=e();for(var h in a)a.hasOwnProperty(h)&&(g[h]=w(a[h]));return v(g,{instantiateEmptyObject:e})}var x="__immutable_invariants_hold",y=["setPrototypeOf"],z=["keys"],A=y.concat(["push","pop","sort","splice","shift","unshift","reverse"]),B=z.concat(["map","filter","slice","concat","reduce","reduceRight"]);e.prototype=Error.prototype;var C=w({});w.isImmutable=c,w.ImmutableError=e,Object.freeze(w),"object"==typeof module?module.exports=w:"object"==typeof exports?exports.Immutable=w:"object"==typeof window?window.Immutable=w:"object"==typeof global&&(global.Immutable=w)}();
\ No newline at end of file
diff --git a/src/seamless-immutable.js b/src/seamless-immutable.js
index 1b6cc75..0a707b7 100644
--- a/src/seamless-immutable.js
+++ b/src/seamless-immutable.js
@@ -88,6 +88,36 @@
     });
   }
 
+  function arraySet(idx, value) {
+    if (idx in this && this[idx] === value) {
+      return this;
+    }
+
+    var mutable = asMutableArray.call(this);
+    mutable[idx] = Immutable(value);
+    return makeImmutableArray(mutable);
+  }
+
+  function arraySetIn(pth, value) {
+    var head = pth[0];
+
+    if (pth.length === 1) {
+      return arraySet.call(this, head, value);
+    } else {
+      var tail = pth.slice(1);
+      // this[head] might (validly) be an (immutable) array or object
+      var newValue = this[head].setIn(tail, value);
+
+      if (head in this && this[head] === newValue) {
+        return this;
+      }
+
+      var mutable = asMutableArray.call(this);
+      mutable[head] = newValue;
+      return makeImmutableArray(mutable);
+    }
+  }
+
   function makeImmutableArray(array) {
     // Don't change their implementations, but wrap these functions to make sure
     // they always return an immutable value.
@@ -101,6 +131,8 @@
     addPropertyTo(array, "flatMap",  flatMap);
     addPropertyTo(array, "asObject", asObject);
     addPropertyTo(array, "asMutable", asMutableArray);
+    addPropertyTo(array, "set", arraySet);
+    addPropertyTo(array, "setIn", arraySetIn);
 
     for(var i = 0, length = array.length; i < length; i++) {
       array[i] = Immutable(array[i]);
@@ -322,6 +354,42 @@
     }
   }
 
+  var immutableEmptyObject = Immutable({});
+
+  function objectSetIn(path, value) {
+    var head = path[0];
+    if (path.length === 1) {
+      return objectSet.call(this, head, value);
+    }
+
+    var tail = path.slice(1);
+    var newValue;
+    if (this.hasOwnProperty(head) && this[head] !== undefined) {
+      // Might (validly) be object or array
+      newValue = this[head].setIn(tail, value);
+    } else {
+      newValue = objectSetIn.call(immutableEmptyObject, tail, value);
+    }
+
+    if (this.hasOwnProperty(head) && this[head] === newValue) {
+      return this;
+    }
+
+    var mutable = quickCopy(this, this.instantiateEmptyObject());
+    mutable[head] = newValue;
+    return makeImmutableObject(mutable, this);
+  }
+
+  function objectSet(property, value) {
+    if (this.hasOwnProperty(property) && this[property] === value) {
+      return this;
+    }
+
+    var mutable = quickCopy(this, this.instantiateEmptyObject());
+    mutable[property] = Immutable(value);
+    return makeImmutableObject(mutable, this);
+  }
+
   function asMutableObject(opts) {
     var result = this.instantiateEmptyObject(), key;
 
@@ -357,6 +425,8 @@
     addPropertyTo(obj, "without", without);
     addPropertyTo(obj, "asMutable", asMutableObject);
     addPropertyTo(obj, "instantiateEmptyObject", instantiateEmptyObject);
+    addPropertyTo(obj, "set", objectSet);
+    addPropertyTo(obj, "setIn", objectSetIn);
 
     return makeImmutable(obj, mutatingObjectMethods);
   }


Test patch:
diff --git a/test/ImmutableArray.spec.js b/test/ImmutableArray.spec.js
index b6b0815..16b463d 100644
--- a/test/ImmutableArray.spec.js
+++ b/test/ImmutableArray.spec.js
@@ -2,6 +2,7 @@ var testCompat    = require("./ImmutableArray/test-compat.js");
 var testFlatMap   = require("./ImmutableArray/test-flatMap.js");
 var testAsObject  = require("./ImmutableArray/test-asObject.js");
 var testAsMutable = require("./ImmutableArray/test-asMutable.js");
+var testSet       = require("./ImmutableArray/test-set.js");
 var devBuild      = require("../seamless-immutable.development.js");
 var prodBuild     = require("../seamless-immutable.production.min.js");
 var getTestUtils  = require("./TestUtils.js");
@@ -19,6 +20,7 @@ var getTestUtils  = require("./TestUtils.js");
       testFlatMap(config);
       testAsObject(config);
       testAsMutable(config);
+      testSet(config);
     });
   });
 });
diff --git a/test/ImmutableArray/test-set.js b/test/ImmutableArray/test-set.js
new file mode 100644
index 0000000..8c751b6
--- /dev/null
+++ b/test/ImmutableArray/test-set.js
@@ -0,0 +1,63 @@
+var JSC          = require("jscheck");
+var assert       = require("chai").assert;
+var _            = require("lodash");
+var getTestUtils = require("../TestUtils.js");
+
+
+module.exports = function(config) {
+  var Immutable = config.implementation;
+  var TestUtils = getTestUtils(Immutable);
+  var check     = TestUtils.check;
+
+  function getPathComponent() {
+    // It's very convenient to use lodash.set, but it has funky behaviour
+    // with numeric keys.
+    var s = JSC.string()().replace(/[^\w]/g, '_');
+    return /^\d+$/.test(s) ? s + 'a' : s;
+  }
+
+  describe("#set", function() {
+    it("sets an array element", function () {
+      check(100, [ JSC.array([TestUtils.TraversableObjectSpecifier, JSC.any()]) ], function(array) {
+        var immutable = Immutable(array);
+        var mutable = immutable.asMutable();
+        var index = JSC.integer(0, array.length);
+        var newValue = JSC.any();
+
+        immutable = immutable.set(index, newValue);
+        mutable[index] = newValue;
+
+        assert.deepEqual(immutable, mutable);
+      });
+    });
+  });
+
+  describe("#setIn", function() {
+    it("sets a property by path", function () {
+      check(100, [ JSC.array([TestUtils.TraversableObjectSpecifier]) ], function(array) {
+        var immutable = Immutable(array);
+        var value = JSC.any()();
+
+        var idx = JSC.integer(0, array.length-1);
+        var key = JSC.one_of(_.keys(immutable[idx]))();
+
+        var util = require('util');
+        function printArr(arr) {
+          return '[\n\t>'+_.map(arr, util.inspect).join('\n\t>')+'\n]';
+        }
+
+        var mutable = immutable.asMutable();
+        assert.deepEqual(immutable, mutable);
+        if (Immutable.isImmutable(mutable[idx])) {
+          mutable[idx] = mutable[idx].asMutable();
+        }
+        mutable[idx][key] = value;
+
+        assert.deepEqual(
+          immutable.setIn([idx, key], value),
+          mutable
+        );
+      });
+    });
+  });
+};
diff --git a/test/ImmutableObject.spec.js b/test/ImmutableObject.spec.js
index 56cfedc..40c4eac 100644
--- a/test/ImmutableObject.spec.js
+++ b/test/ImmutableObject.spec.js
@@ -2,6 +2,7 @@ var testMerge     = require("./ImmutableObject/test-merge.js");
 var testCompat    = require("./ImmutableObject/test-compat.js");
 var testWithout   = require("./ImmutableObject/test-without.js");
 var testAsMutable = require("./ImmutableObject/test-asMutable.js");
+var testSet = require("./ImmutableObject/test-set.js");
 var testInstantiateEmptyObject = require("./ImmutableObject/test-instantiateEmptyObject.js");
 var devBuild      = require("../seamless-immutable.development.js");
 var prodBuild     = require("../seamless-immutable.production.min.js");
@@ -21,6 +22,7 @@ var getTestUtils  = require("./TestUtils.js");
       testWithout(config);
       testAsMutable(config);
       testInstantiateEmptyObject(config);
+      testSet(config);
     });
   });
 });
diff --git a/test/ImmutableObject/test-set.js b/test/ImmutableObject/test-set.js
new file mode 100644
index 0000000..e717409
--- /dev/null
+++ b/test/ImmutableObject/test-set.js
@@ -0,0 +1,56 @@
+var JSC          = require("jscheck");
+var assert       = require("chai").assert;
+var _            = require("lodash");
+var getTestUtils = require("../TestUtils.js");
+
+
+module.exports = function(config) {
+  var Immutable = config.implementation;
+  var TestUtils = getTestUtils(Immutable);
+  var check     = TestUtils.check;
+
+  function getPathComponent() {
+    // It's very convenient to use lodash.set, but it has funky behaviour
+    // with numeric keys.
+    var s = JSC.string()().replace(/[^\w]/g, '_');
+    return /^\d+$/.test(s) ? s + 'a' : s;
+  }
+
+  describe("#set", function() {
+    it("sets a property by name", function () {
+      check(100, [TestUtils.ComplexObjectSpecifier()], function(ob) {
+        var immutable = Immutable(ob);
+        var mutable = immutable.asMutable();
+        var prop = getPathComponent();
+        var value = JSC.any()();
+
+        assert.deepEqual(
+          immutable.set(prop, value),
+          _.set(mutable, prop, value)
+        );
+      });
+    });
+  });
+
+  describe("#setIn", function() {
+    it("sets a property by path", function () {
+      check(100, [TestUtils.ComplexObjectSpecifier()], function(ob) {
+        var immutable = Immutable(ob);
+        var mutable = immutable.asMutable();
+        var value = JSC.any()();
+
+        assert.deepEqual(immutable, mutable);
+
+        var path = [], depth = JSC.integer(1, 5)();
+        for (var j = 0; j < depth; j++) {
+          path.push(getPathComponent());
+        }
+
+        assert.deepEqual(
+          immutable.setIn(path, value),
+          _.set(mutable, path, value)
+        );
+      });
+    });
+  });
+};