col-cache.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. const addressRegex = /^[A-Z]+\d+$/;
  2. // =========================================================================
  3. // Column Letter to Number conversion
  4. const colCache = {
  5. _dictionary: [
  6. 'A',
  7. 'B',
  8. 'C',
  9. 'D',
  10. 'E',
  11. 'F',
  12. 'G',
  13. 'H',
  14. 'I',
  15. 'J',
  16. 'K',
  17. 'L',
  18. 'M',
  19. 'N',
  20. 'O',
  21. 'P',
  22. 'Q',
  23. 'R',
  24. 'S',
  25. 'T',
  26. 'U',
  27. 'V',
  28. 'W',
  29. 'X',
  30. 'Y',
  31. 'Z',
  32. ],
  33. _l2nFill: 0,
  34. _l2n: {},
  35. _n2l: [],
  36. _level(n) {
  37. if (n <= 26) {
  38. return 1;
  39. }
  40. if (n <= 26 * 26) {
  41. return 2;
  42. }
  43. return 3;
  44. },
  45. _fill(level) {
  46. let c;
  47. let v;
  48. let l1;
  49. let l2;
  50. let l3;
  51. let n = 1;
  52. if (level >= 4) {
  53. throw new Error('Out of bounds. Excel supports columns from 1 to 16384');
  54. }
  55. if (this._l2nFill < 1 && level >= 1) {
  56. while (n <= 26) {
  57. c = this._dictionary[n - 1];
  58. this._n2l[n] = c;
  59. this._l2n[c] = n;
  60. n++;
  61. }
  62. this._l2nFill = 1;
  63. }
  64. if (this._l2nFill < 2 && level >= 2) {
  65. n = 27;
  66. while (n <= 26 + (26 * 26)) {
  67. v = n - (26 + 1);
  68. l1 = v % 26;
  69. l2 = Math.floor(v / 26);
  70. c = this._dictionary[l2] + this._dictionary[l1];
  71. this._n2l[n] = c;
  72. this._l2n[c] = n;
  73. n++;
  74. }
  75. this._l2nFill = 2;
  76. }
  77. if (this._l2nFill < 3 && level >= 3) {
  78. n = 26 + (26 * 26) + 1;
  79. while (n <= 16384) {
  80. v = n - ((26 * 26) + 26 + 1);
  81. l1 = v % 26;
  82. l2 = Math.floor(v / 26) % 26;
  83. l3 = Math.floor(v / (26 * 26));
  84. c = this._dictionary[l3] + this._dictionary[l2] + this._dictionary[l1];
  85. this._n2l[n] = c;
  86. this._l2n[c] = n;
  87. n++;
  88. }
  89. this._l2nFill = 3;
  90. }
  91. },
  92. l2n(l) {
  93. if (!this._l2n[l]) {
  94. this._fill(l.length);
  95. }
  96. if (!this._l2n[l]) {
  97. throw new Error(`Out of bounds. Invalid column letter: ${l}`);
  98. }
  99. return this._l2n[l];
  100. },
  101. n2l(n) {
  102. if (n < 1 || n > 16384) {
  103. throw new Error(`${n} is out of bounds. Excel supports columns from 1 to 16384`);
  104. }
  105. if (!this._n2l[n]) {
  106. this._fill(this._level(n));
  107. }
  108. return this._n2l[n];
  109. },
  110. // =========================================================================
  111. // Address processing
  112. _hash: {},
  113. // check if value looks like an address
  114. validateAddress(value) {
  115. if (!addressRegex.test(value)) {
  116. throw new Error(`Invalid Address: ${value}`);
  117. }
  118. return true;
  119. },
  120. // convert address string into structure
  121. decodeAddress(value) {
  122. const addr = value.length < 5 && this._hash[value];
  123. if (addr) {
  124. return addr;
  125. }
  126. let hasCol = false;
  127. let col = '';
  128. let colNumber = 0;
  129. let hasRow = false;
  130. let row = '';
  131. let rowNumber = 0;
  132. for (let i = 0, char; i < value.length; i++) {
  133. char = value.charCodeAt(i);
  134. // col should before row
  135. if (!hasRow && char >= 65 && char <= 90) {
  136. // 65 = 'A'.charCodeAt(0)
  137. // 90 = 'Z'.charCodeAt(0)
  138. hasCol = true;
  139. col += value[i];
  140. // colNumber starts from 1
  141. colNumber = (colNumber * 26) + char - 64;
  142. } else if (char >= 48 && char <= 57) {
  143. // 48 = '0'.charCodeAt(0)
  144. // 57 = '9'.charCodeAt(0)
  145. hasRow = true;
  146. row += value[i];
  147. // rowNumber starts from 0
  148. rowNumber = (rowNumber * 10) + char - 48;
  149. } else if (hasRow && hasCol && char !== 36) {
  150. // 36 = '$'.charCodeAt(0)
  151. break;
  152. }
  153. }
  154. if (!hasCol) {
  155. colNumber = undefined;
  156. } else if (colNumber > 16384) {
  157. throw new Error(`Out of bounds. Invalid column letter: ${col}`);
  158. }
  159. if (!hasRow) {
  160. rowNumber = undefined;
  161. }
  162. // in case $row$col
  163. value = col + row;
  164. const address = {
  165. address: value,
  166. col: colNumber,
  167. row: rowNumber,
  168. $col$row: `$${col}$${row}`,
  169. };
  170. // mem fix - cache only the tl 100x100 square
  171. if (colNumber <= 100 && rowNumber <= 100) {
  172. this._hash[value] = address;
  173. this._hash[address.$col$row] = address;
  174. }
  175. return address;
  176. },
  177. // convert r,c into structure (if only 1 arg, assume r is address string)
  178. getAddress(r, c) {
  179. if (c) {
  180. const address = this.n2l(c) + r;
  181. return this.decodeAddress(address);
  182. }
  183. return this.decodeAddress(r);
  184. },
  185. // convert [address], [tl:br] into address structures
  186. decode(value) {
  187. const parts = value.split(':');
  188. if (parts.length === 2) {
  189. const tl = this.decodeAddress(parts[0]);
  190. const br = this.decodeAddress(parts[1]);
  191. const result = {
  192. top: Math.min(tl.row, br.row),
  193. left: Math.min(tl.col, br.col),
  194. bottom: Math.max(tl.row, br.row),
  195. right: Math.max(tl.col, br.col),
  196. };
  197. // reconstruct tl, br and dimensions
  198. result.tl = this.n2l(result.left) + result.top;
  199. result.br = this.n2l(result.right) + result.bottom;
  200. result.dimensions = `${result.tl}:${result.br}`;
  201. return result;
  202. }
  203. return this.decodeAddress(value);
  204. },
  205. // convert [sheetName!][$]col[$]row[[$]col[$]row] into address or range structures
  206. decodeEx(value) {
  207. const groups = value.match(/(?:(?:(?:'((?:[^']|'')*)')|([^'^ !]*))!)?(.*)/);
  208. const sheetName = groups[1] || groups[2]; // Qouted and unqouted groups
  209. const reference = groups[3]; // Remaining address
  210. const parts = reference.split(':');
  211. if (parts.length > 1) {
  212. let tl = this.decodeAddress(parts[0]);
  213. let br = this.decodeAddress(parts[1]);
  214. const top = Math.min(tl.row, br.row);
  215. const left = Math.min(tl.col, br.col);
  216. const bottom = Math.max(tl.row, br.row);
  217. const right = Math.max(tl.col, br.col);
  218. tl = this.n2l(left) + top;
  219. br = this.n2l(right) + bottom;
  220. return {
  221. top,
  222. left,
  223. bottom,
  224. right,
  225. sheetName,
  226. tl: {address: tl, col: left, row: top, $col$row: `$${this.n2l(left)}$${top}`, sheetName},
  227. br: {
  228. address: br,
  229. col: right,
  230. row: bottom,
  231. $col$row: `$${this.n2l(right)}$${bottom}`,
  232. sheetName,
  233. },
  234. dimensions: `${tl}:${br}`,
  235. };
  236. }
  237. if (reference.startsWith('#')) {
  238. return sheetName ? {sheetName, error: reference} : {error: reference};
  239. }
  240. const address = this.decodeAddress(reference);
  241. return sheetName ? {sheetName, ...address} : address;
  242. },
  243. // convert row,col into address string
  244. encodeAddress(row, col) {
  245. return colCache.n2l(col) + row;
  246. },
  247. // convert row,col into string address or t,l,b,r into range
  248. encode() {
  249. switch (arguments.length) {
  250. case 2:
  251. return colCache.encodeAddress(arguments[0], arguments[1]);
  252. case 4:
  253. return `${colCache.encodeAddress(arguments[0], arguments[1])}:${colCache.encodeAddress(
  254. arguments[2],
  255. arguments[3]
  256. )}`;
  257. default:
  258. throw new Error('Can only encode with 2 or 4 arguments');
  259. }
  260. },
  261. // return true if address is contained within range
  262. inRange(range, address) {
  263. const [left, top, , right, bottom] = range;
  264. const [col, row] = address;
  265. return col >= left && col <= right && row >= top && row <= bottom;
  266. },
  267. };
  268. module.exports = colCache;