FieldRangeExpression.js 6.5 KB

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