FieldRangeExpression.js 6.4 KB

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