number.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. 'use strict';
  2. // Load modules
  3. const Any = require('./any');
  4. const Ref = require('./ref');
  5. const Hoek = require('hoek');
  6. // Declare internals
  7. const internals = {
  8. precisionRx: /(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/
  9. };
  10. internals.Number = class extends Any {
  11. constructor() {
  12. super();
  13. this._type = 'number';
  14. this._invalids.add(Infinity);
  15. this._invalids.add(-Infinity);
  16. }
  17. _base(value, state, options) {
  18. const result = {
  19. errors: null,
  20. value
  21. };
  22. if (typeof value === 'string' &&
  23. options.convert) {
  24. const number = parseFloat(value);
  25. result.value = (isNaN(number) || !isFinite(value)) ? NaN : number;
  26. }
  27. const isNumber = typeof result.value === 'number' && !isNaN(result.value);
  28. if (options.convert && 'precision' in this._flags && isNumber) {
  29. // This is conceptually equivalent to using toFixed but it should be much faster
  30. const precision = Math.pow(10, this._flags.precision);
  31. result.value = Math.round(result.value * precision) / precision;
  32. }
  33. result.errors = isNumber ? null : this.createError('number.base', null, state, options);
  34. return result;
  35. }
  36. multiple(base) {
  37. const isRef = Ref.isRef(base);
  38. if (!isRef) {
  39. Hoek.assert(typeof base === 'number' && isFinite(base), 'multiple must be a number');
  40. Hoek.assert(base > 0, 'multiple must be greater than 0');
  41. }
  42. return this._test('multiple', base, function (value, state, options) {
  43. const divisor = isRef ? base(state.parent, options) : base;
  44. if (isRef && (typeof divisor !== 'number' || !isFinite(divisor))) {
  45. return this.createError('number.ref', { ref: base.key }, state, options);
  46. }
  47. if (value % divisor === 0) {
  48. return value;
  49. }
  50. return this.createError('number.multiple', { multiple: base, value }, state, options);
  51. });
  52. }
  53. integer() {
  54. return this._test('integer', undefined, function (value, state, options) {
  55. return Hoek.isInteger(value) ? value : this.createError('number.integer', { value }, state, options);
  56. });
  57. }
  58. negative() {
  59. return this._test('negative', undefined, function (value, state, options) {
  60. if (value < 0) {
  61. return value;
  62. }
  63. return this.createError('number.negative', { value }, state, options);
  64. });
  65. }
  66. positive() {
  67. return this._test('positive', undefined, function (value, state, options) {
  68. if (value > 0) {
  69. return value;
  70. }
  71. return this.createError('number.positive', { value }, state, options);
  72. });
  73. }
  74. precision(limit) {
  75. Hoek.assert(Hoek.isInteger(limit), 'limit must be an integer');
  76. Hoek.assert(!('precision' in this._flags), 'precision already set');
  77. const obj = this._test('precision', limit, function (value, state, options) {
  78. const places = value.toString().match(internals.precisionRx);
  79. const decimals = Math.max((places[1] ? places[1].length : 0) - (places[2] ? parseInt(places[2], 10) : 0), 0);
  80. if (decimals <= limit) {
  81. return value;
  82. }
  83. return this.createError('number.precision', { limit, value }, state, options);
  84. });
  85. obj._flags.precision = limit;
  86. return obj;
  87. }
  88. };
  89. internals.compare = function (type, compare) {
  90. return function (limit) {
  91. const isRef = Ref.isRef(limit);
  92. const isNumber = typeof limit === 'number' && !isNaN(limit);
  93. Hoek.assert(isNumber || isRef, 'limit must be a number or reference');
  94. return this._test(type, limit, function (value, state, options) {
  95. let compareTo;
  96. if (isRef) {
  97. compareTo = limit(state.parent, options);
  98. if (!(typeof compareTo === 'number' && !isNaN(compareTo))) {
  99. return this.createError('number.ref', { ref: limit.key }, state, options);
  100. }
  101. }
  102. else {
  103. compareTo = limit;
  104. }
  105. if (compare(value, compareTo)) {
  106. return value;
  107. }
  108. return this.createError('number.' + type, { limit: compareTo, value }, state, options);
  109. });
  110. };
  111. };
  112. internals.Number.prototype.min = internals.compare('min', (value, limit) => value >= limit);
  113. internals.Number.prototype.max = internals.compare('max', (value, limit) => value <= limit);
  114. internals.Number.prototype.greater = internals.compare('greater', (value, limit) => value > limit);
  115. internals.Number.prototype.less = internals.compare('less', (value, limit) => value < limit);
  116. module.exports = new internals.Number();