range.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. // @ts-check
  2. class Range {
  3. /**
  4. * @param {number} pos
  5. * @param {number} length
  6. */
  7. constructor(pos, length) {
  8. this.pos = pos
  9. this.length = length
  10. }
  11. get firstIndex() {
  12. return this.pos
  13. }
  14. /**
  15. * @returns {number} for an empty range, lastIndex will be -1
  16. */
  17. get lastIndex() {
  18. if (this.length === 0) {
  19. throw new Error('Range is empty')
  20. }
  21. return this.pos + this.length - 1
  22. }
  23. get firstNextIndex() {
  24. return this.pos + this.length
  25. }
  26. /**
  27. * @param {Range} range
  28. * @returns {boolean}
  29. */
  30. startsAfter(range) {
  31. return this.firstIndex >= range.firstNextIndex
  32. }
  33. /**
  34. * @param {number} pos
  35. * @returns {boolean}
  36. */
  37. firstIndexIsAfter(pos) {
  38. return this.firstIndex > pos
  39. }
  40. /**
  41. *
  42. * @returns {boolean}
  43. */
  44. isEmpty() {
  45. return this.length === 0
  46. }
  47. /**
  48. * checks if the range contains a given range
  49. * @param {Range} range
  50. */
  51. contains(range) {
  52. return (
  53. this.firstIndex <= range.firstIndex && this.lastIndex >= range.lastIndex
  54. )
  55. }
  56. /**
  57. * checks if the range contains a given index
  58. * @param {number} index
  59. */
  60. containsIndex(index) {
  61. return this.firstIndex <= index && this.lastIndex >= index
  62. }
  63. /**
  64. * @param {Range} range
  65. */
  66. overlaps(range) {
  67. return (
  68. this.firstIndex < range.firstNextIndex &&
  69. this.firstNextIndex > range.firstIndex
  70. )
  71. }
  72. /**
  73. * checks if the range touches a given range
  74. * @param {Range} range
  75. */
  76. touches(range) {
  77. return (
  78. this.firstNextIndex === range.firstIndex ||
  79. this.firstIndex === range.firstNextIndex
  80. )
  81. }
  82. /**
  83. * @param {Range} range
  84. * @returns {number} the length of the intersected range
  85. */
  86. subtract(range) {
  87. if (this.contains(range)) {
  88. this.length -= range.length
  89. return range.length
  90. }
  91. if (range.contains(this)) {
  92. const intersectedLength = this.length
  93. this.length = 0
  94. return intersectedLength
  95. }
  96. if (range.overlaps(this)) {
  97. if (range.firstIndex < this.firstIndex) {
  98. const intersectedLength = range.lastIndex - this.firstIndex + 1
  99. this.length -= intersectedLength
  100. this.pos = range.pos
  101. return intersectedLength
  102. } else {
  103. const intersectedLength = this.lastIndex - range.firstIndex + 1
  104. this.length -= intersectedLength
  105. return intersectedLength
  106. }
  107. }
  108. return 0
  109. }
  110. /**
  111. * @param {Range} range
  112. * @returns {boolean}
  113. */
  114. canMerge(range) {
  115. return this.overlaps(range) || this.touches(range)
  116. }
  117. /**
  118. * @param {Range} range
  119. */
  120. merge(range) {
  121. if (!this.canMerge(range)) {
  122. throw new Error('Ranges cannot be merged')
  123. }
  124. const newFirstIndex = Math.min(this.pos, range.pos)
  125. const newLastIndex = Math.max(this.lastIndex, range.lastIndex)
  126. this.pos = newFirstIndex
  127. this.length = newLastIndex + 1 - newFirstIndex
  128. }
  129. /**
  130. * Moves the range by a given number
  131. * @param {number} length
  132. */
  133. moveBy(length) {
  134. this.pos += length
  135. }
  136. /**
  137. * Extends the range by a given number
  138. * @param {number} extensionLength
  139. */
  140. extendBy(extensionLength) {
  141. this.length += extensionLength
  142. }
  143. /**
  144. * Splits a range on the index and insert a range with the length provided
  145. * @param {number} index
  146. * @param {number} length
  147. * @returns {[Range, Range, Range]}
  148. */
  149. insertAt(index, length) {
  150. if (!this.containsIndex(index)) {
  151. throw new Error('The index must be contained in the range')
  152. }
  153. const rangeUpToIndex = new Range(this.pos, index - this.pos)
  154. const insertedRange = new Range(index, length)
  155. const rangeAfterIndex = new Range(
  156. index + length,
  157. this.length - rangeUpToIndex.length
  158. )
  159. return [rangeUpToIndex, insertedRange, rangeAfterIndex]
  160. }
  161. toRaw() {
  162. return {
  163. pos: this.pos,
  164. length: this.length,
  165. }
  166. }
  167. static fromRaw(raw) {
  168. return new Range(raw.pos, raw.length)
  169. }
  170. }
  171. module.exports = Range