FieldRangeExpression.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. "use strict";
  2. var FieldRangeExpression = module.exports = (function(){
  3. // CONSTRUCTOR
  4. /**
  5. * Create a field range expression.
  6. *
  7. * Field ranges are meant to match up with classic Matcher semantics, and therefore are conjunctions.
  8. *
  9. * For example, these appear in mongo shell predicates in one of these forms:
  10. * { a : C } -> (a == C) // degenerate "point" range
  11. * { a : { $lt : C } } -> (a < C) // open range
  12. * { a : { $gt : C1, $lte : C2 } } -> ((a > C1) && (a <= C2)) // closed
  13. *
  14. * When initially created, a field range only includes one end of the range. Additional points may be added via intersect().
  15. *
  16. * Note that NE and CMP are not supported.
  17. *
  18. * @class FieldRangeExpression
  19. * @namespace mungedb.aggregate.pipeline.expressions
  20. * @module mungedb-aggregate
  21. * @extends munge.pipeline.expressions.Expression
  22. * @constructor
  23. * @param pathExpr the field path for extracting the field value
  24. * @param cmpOp the comparison operator
  25. * @param value the value to compare against
  26. * @returns the newly created field range expression
  27. **/
  28. var klass = function FieldRangeExpression(pathExpr, cmpOp, value){
  29. if(arguments.length !== 3) throw new Error("args expected: pathExpr, cmpOp, and value");
  30. this.pathExpr = pathExpr;
  31. this.range = new Range({cmpOp:cmpOp, value:value});
  32. }, Expression = require("./Expression"), base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
  33. // DEPENDENCIES
  34. var Value = require("../Value"),
  35. ConstantExpression = require("./ConstantExpression");
  36. // NESTED CLASSES
  37. var Range = (function(){
  38. /**
  39. * create a new Range; opts is either {cmpOp:..., value:...} or {bottom:..., isBottomOpen:..., top:..., isTopOpen:...}
  40. * @private
  41. **/
  42. var klass = function Range(opts){
  43. this.isBottomOpen = this.isTopOpen = false;
  44. this.bottom = this.top = undefined;
  45. if(opts.hasOwnProperty("cmpOp") && opts.hasOwnProperty("value")){
  46. switch (opts.cmpOp) {
  47. case Expression.CmpOp.EQ:
  48. this.bottom = this.top = opts.value;
  49. break;
  50. case Expression.CmpOp.GT:
  51. this.isBottomOpen = true;
  52. /* falls through */
  53. case Expression.CmpOp.GTE:
  54. this.isTopOpen = true;
  55. this.bottom = opts.value;
  56. break;
  57. case Expression.CmpOp.LT:
  58. this.isTopOpen = true;
  59. /* falls through */
  60. case Expression.CmpOp.LTE:
  61. this.isBottomOpen = true;
  62. this.top = opts.value;
  63. break;
  64. case Expression.CmpOp.NE:
  65. case Expression.CmpOp.CMP:
  66. throw new Error("CmpOp not allowed: " + opts.cmpOp);
  67. default:
  68. throw new Error("Unexpected CmpOp: " + opts.cmpOp);
  69. }
  70. }else{
  71. this.bottom = opts.bottom;
  72. this.isBottomOpen = opts.isBottomOpen;
  73. this.top = opts.top;
  74. this.isTopOpen = opts.isTopOpen;
  75. }
  76. }, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
  77. // PROTOTYPE MEMBERS
  78. proto.intersect = function intersect(range){
  79. // Find the max of the bottom end of ranges
  80. var maxBottom = range.bottom,
  81. maxBottomOpen = range.isBottomOpen;
  82. if(this.bottom !== undefined){
  83. if(range.bottom === undefined){
  84. maxBottom = this.bottom;
  85. maxBottomOpen = this.isBottomOpen;
  86. }else{
  87. if(Value.compare(this.bottom, range.bottom) === 0){
  88. maxBottomOpen = this.isBottomOpen || range.isBottomOpen;
  89. }else{
  90. maxBottom = this.bottom;
  91. maxBottomOpen = this.isBottomOpen;
  92. }
  93. }
  94. }
  95. // Find the min of the tops of the ranges
  96. var minTop = range.top,
  97. minTopOpen = range.isTopOpen;
  98. if(this.top !== undefined){
  99. if(range.top === undefined){
  100. minTop = this.top;
  101. minTopOpen = this.isTopOpen;
  102. }else{
  103. if(Value.compare(this.top, range.top) === 0){
  104. minTopOpen = this.isTopOpen || range.isTopOpen;
  105. }else{
  106. minTop = this.top;
  107. minTopOpen = this.isTopOpen;
  108. }
  109. }
  110. }
  111. if(Value.compare(maxBottom, minTop) <= 0)
  112. return new Range({bottom:maxBottom, isBottomOpen:maxBottomOpen, top:minTop, isTopOpen:minTopOpen});
  113. return null; // empty intersection
  114. };
  115. proto.contains = function contains(value){
  116. var cmp;
  117. if(this.bottom !== undefined){
  118. cmp = Value.compare(value, this.bottom);
  119. if(cmp < 0) return false;
  120. if(this.isBottomOpen && cmp === 0) return false;
  121. }
  122. if(this.top !== undefined){
  123. cmp = Value.compare(value, this.top);
  124. if(cmp > 0) return false;
  125. if(this.isTopOpen && cmp === 0) return false;
  126. }
  127. return true;
  128. };
  129. return klass;
  130. })();
  131. // PROTOTYPE MEMBERS
  132. proto.evaluate = function evaluate(obj){
  133. if(this.range === undefined) return false;
  134. var value = this.pathExpr.evaluate(obj);
  135. return this.range.contains(value);
  136. };
  137. proto.optimize = function optimize(){
  138. if(this.range === undefined) return new ConstantExpression(false);
  139. if(this.range.bottom === undefined && this.range.top === undefined) return new ConstantExpression(true);
  140. return this;
  141. };
  142. proto.addDependencies = function(deps){
  143. return this.pathExpr.addDependencies(deps);
  144. };
  145. /**
  146. * Add an intersecting range.
  147. *
  148. * This can be done any number of times after creation. The range is
  149. * internally optimized for each new addition. If the new intersection
  150. * extends or reduces the values within the range, the internal
  151. * representation is adjusted to reflect that.
  152. *
  153. * Note that NE and CMP are not supported.
  154. *
  155. * @method intersect
  156. * @param cmpOp the comparison operator
  157. * @param pValue the value to compare against
  158. **/
  159. proto.intersect = function intersect(cmpOp, value){
  160. this.range = this.range.intersect(new Range({cmpOp:cmpOp, value:value}));
  161. };
  162. proto.toJson = function toJson(){
  163. if (this.range === undefined) return false; //nothing will satisfy this predicate
  164. if (this.range.top === undefined && this.range.bottom === undefined) return true; // any value will satisfy this predicate
  165. // FIXME Append constant values using the $const operator. SERVER-6769
  166. var json = {};
  167. if (this.range.top === this.range.bottom) {
  168. json[Expression.CmpOp.EQ] = [this.pathExpr.toJson(), this.range.top];
  169. }else{
  170. var leftOp = {};
  171. if (this.range.bottom !== undefined) {
  172. leftOp[this.range.isBottomOpen ? Expression.CmpOp.GT : Expression.CmpOp.GTE] = [this.pathExpr.toJson(), this.range.bottom];
  173. if (this.range.top === undefined) return leftOp;
  174. }
  175. var rightOp = {};
  176. if(this.range.top !== undefined){
  177. rightOp[this.range.isTopOpen ? Expression.CmpOp.LT : Expression.CmpOp.LTE] = [this.pathExpr.toJson(), this.range.top];
  178. if (this.range.bottom === undefined) return rightOp;
  179. }
  180. json.$and = [leftOp, rightOp];
  181. }
  182. return json;
  183. };
  184. //TODO: proto.addToBson = ...?
  185. //TODO: proto.addToBsonObj = ...?
  186. //TODO: proto.addToBsonArray = ...?
  187. //TODO: proto.toMatcherBson = ...? WILL PROBABLY NEED THESE...
  188. return klass;
  189. })();