convert-strings-to-dates.spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. import type { DateConvertible } from './convert-strings-to-dates';
  2. import { convertStringsToDates } from './convert-strings-to-dates';
  3. describe('convertStringsToDates', () => {
  4. // Test case 1: Basic conversion in a flat object
  5. test('should convert ISO date strings to Date objects in a flat object', () => {
  6. const dateString = '2023-01-15T10:00:00.000Z';
  7. const input = {
  8. id: 1,
  9. createdAt: dateString,
  10. name: 'Test Item',
  11. };
  12. const expected = {
  13. id: 1,
  14. createdAt: new Date(dateString),
  15. name: 'Test Item',
  16. };
  17. const result = convertStringsToDates(input) as Record<
  18. string,
  19. DateConvertible
  20. >;
  21. expect(result.createdAt).toBeInstanceOf(Date);
  22. if (result.createdAt instanceof Date) {
  23. expect(result.createdAt.toISOString()).toEqual(dateString);
  24. }
  25. expect(result).toEqual(expected);
  26. });
  27. // Test case 2: Nested objects
  28. test('should recursively convert ISO date strings in nested objects', () => {
  29. const dateString1 = '2023-02-20T12:30:00.000Z';
  30. const dateString2 = '2023-03-01T08:00:00.000Z';
  31. const input = {
  32. data: {
  33. item1: {
  34. updatedAt: dateString1,
  35. value: 10,
  36. },
  37. item2: {
  38. nested: {
  39. deletedAt: dateString2,
  40. isActive: false,
  41. },
  42. },
  43. },
  44. };
  45. const expected = {
  46. data: {
  47. item1: {
  48. updatedAt: new Date(dateString1),
  49. value: 10,
  50. },
  51. item2: {
  52. nested: {
  53. deletedAt: new Date(dateString2),
  54. isActive: false,
  55. },
  56. },
  57. },
  58. };
  59. const result = convertStringsToDates(input) as {
  60. data: {
  61. item1: {
  62. updatedAt: DateConvertible; // Assert 'updatedAt' later
  63. value: number;
  64. };
  65. item2: {
  66. nested: {
  67. deletedAt: DateConvertible; // Assert 'deletedAt' later
  68. isActive: boolean;
  69. };
  70. };
  71. };
  72. };
  73. expect(result.data.item1.updatedAt).toBeInstanceOf(Date);
  74. if (result.data.item1.updatedAt instanceof Date) {
  75. expect(result.data.item1.updatedAt.toISOString()).toEqual(dateString1);
  76. expect(result.data.item2.nested.deletedAt).toBeInstanceOf(Date);
  77. }
  78. if (result.data.item2.nested.deletedAt instanceof Date) {
  79. expect(result.data.item2.nested.deletedAt).toBeInstanceOf(Date);
  80. expect(result.data.item2.nested.deletedAt.toISOString()).toEqual(
  81. dateString2,
  82. );
  83. }
  84. expect(result).toEqual(expected);
  85. });
  86. // Test case 3: Arrays of objects
  87. test('should recursively convert ISO date strings in arrays of objects', () => {
  88. const dateString1 = '2023-04-05T14:15:00.000Z';
  89. const dateString2 = '2023-05-10T16:00:00.000Z';
  90. const input = [
  91. { id: 1, eventDate: dateString1 },
  92. { id: 2, eventDate: dateString2, data: { nestedProp: 'value' } },
  93. ];
  94. const expected = [
  95. { id: 1, eventDate: new Date(dateString1) },
  96. {
  97. id: 2,
  98. eventDate: new Date(dateString2),
  99. data: { nestedProp: 'value' },
  100. },
  101. ];
  102. const result = convertStringsToDates(input) as [
  103. { id: number; eventDate: DateConvertible },
  104. { id: number; eventDate: DateConvertible; data: { nestedProp: string } },
  105. ];
  106. expect(result[0].eventDate).toBeInstanceOf(Date);
  107. if (result[0].eventDate instanceof Date) {
  108. expect(result[0].eventDate.toISOString()).toEqual(dateString1);
  109. }
  110. if (result[1].eventDate instanceof Date) {
  111. expect(result[1].eventDate).toBeInstanceOf(Date);
  112. expect(result[1].eventDate.toISOString()).toEqual(dateString2);
  113. }
  114. expect(result).toEqual(expected);
  115. });
  116. // Test case 4: Array containing date strings directly (though less common for this function)
  117. test('should handle arrays containing date strings directly', () => {
  118. const dateString = '2023-06-20T18:00:00.000Z';
  119. const input: [string, string, number] = ['text', dateString, 123];
  120. const expected = ['text', new Date(dateString), 123];
  121. const result = convertStringsToDates(input) as DateConvertible[];
  122. expect(result[1]).toBeInstanceOf(Date);
  123. if (result[1] instanceof Date) {
  124. expect(result[1].toISOString()).toEqual(dateString);
  125. }
  126. expect(result).toEqual(expected);
  127. });
  128. // Test case 5: Data without date strings should remain unchanged
  129. test('should not modify data without ISO date strings', () => {
  130. const input = {
  131. name: 'Product A',
  132. price: 99.99,
  133. tags: ['electronic', 'sale'],
  134. description: 'Some text',
  135. };
  136. const originalInput = JSON.parse(JSON.stringify(input)); // Deep copy to ensure no mutation
  137. const result = convertStringsToDates(input);
  138. expect(result).toEqual(originalInput); // Should be deeply equal
  139. expect(result).not.toBe(input); // Confirm it mutated the original object
  140. });
  141. // Test case 6: Null, undefined, and primitive values
  142. test('should return primitive values as is', () => {
  143. expect(convertStringsToDates(null)).toBeNull();
  144. expect(convertStringsToDates(undefined)).toBeUndefined();
  145. expect(convertStringsToDates(123)).toBe(123);
  146. expect(convertStringsToDates('hello')).toBe('hello');
  147. expect(convertStringsToDates(true)).toBe(true);
  148. });
  149. // Test case 7: Edge case - empty objects/arrays
  150. test('should handle empty objects and arrays correctly', () => {
  151. const emptyObject = {};
  152. const emptyArray = [];
  153. expect(convertStringsToDates(emptyObject)).toEqual({});
  154. expect(convertStringsToDates(emptyArray)).toEqual([]);
  155. expect(convertStringsToDates(emptyObject)).not.toBe(emptyObject);
  156. expect(convertStringsToDates(emptyArray)).toEqual(emptyArray);
  157. });
  158. // Test case 8: Date string with different milliseconds (isoDateRegex without .000)
  159. test('should handle date strings with varied milliseconds', () => {
  160. const dateString = '2023-01-15T10:00:00Z'; // No milliseconds
  161. const input = { createdAt: dateString };
  162. const expected = { createdAt: new Date(dateString) };
  163. const result = convertStringsToDates(input) as Record<
  164. string,
  165. DateConvertible
  166. >;
  167. expect(result.createdAt).toBeInstanceOf(Date);
  168. if (result.createdAt instanceof Date) {
  169. expect(result.createdAt.toISOString()).toEqual(
  170. '2023-01-15T10:00:00.000Z',
  171. );
  172. }
  173. expect(result).toEqual(expected);
  174. });
  175. // Test case 9: Object with null properties
  176. test('should handle objects with null properties', () => {
  177. const dateString = '2023-07-01T00:00:00.000Z';
  178. const input = {
  179. prop1: dateString,
  180. prop2: null,
  181. prop3: {
  182. nestedNull: null,
  183. nestedDate: dateString,
  184. },
  185. };
  186. const expected = {
  187. prop1: new Date(dateString),
  188. prop2: null,
  189. prop3: {
  190. nestedNull: null,
  191. nestedDate: new Date(dateString),
  192. },
  193. };
  194. const result = convertStringsToDates(input) as {
  195. prop1: DateConvertible;
  196. prop2: null;
  197. prop3: {
  198. nestedNull: null;
  199. nestedDate: DateConvertible;
  200. };
  201. };
  202. expect(result.prop1).toBeInstanceOf(Date);
  203. expect(result.prop3.nestedDate).toBeInstanceOf(Date);
  204. expect(result).toEqual(expected);
  205. });
  206. // Test case 10: Date string with UTC offset (e.g., +09:00)
  207. test('should convert ISO date strings with UTC offset to Date objects', () => {
  208. const dateStringWithOffset = '2025-06-12T14:00:00+09:00';
  209. const input = {
  210. id: 2,
  211. eventTime: dateStringWithOffset,
  212. details: {
  213. lastActivity: '2025-06-12T05:00:00-04:00',
  214. },
  215. };
  216. const expected = {
  217. id: 2,
  218. eventTime: new Date(dateStringWithOffset),
  219. details: {
  220. lastActivity: new Date('2025-06-12T05:00:00-04:00'),
  221. },
  222. };
  223. const result = convertStringsToDates(input) as {
  224. id: number;
  225. eventTime: DateConvertible;
  226. details: {
  227. lastActivity: DateConvertible;
  228. };
  229. };
  230. expect(result.eventTime).toBeInstanceOf(Date);
  231. if (result.eventTime instanceof Date) {
  232. expect(result.eventTime.toISOString()).toEqual(
  233. new Date(dateStringWithOffset).toISOString(),
  234. );
  235. }
  236. expect(result.details.lastActivity).toBeInstanceOf(Date);
  237. if (result.details.lastActivity instanceof Date) {
  238. expect(result.details.lastActivity.toISOString()).toEqual(
  239. new Date('2025-06-12T05:00:00-04:00').toISOString(),
  240. );
  241. }
  242. expect(result).toEqual(expected);
  243. });
  244. // Test case 11: Date string with negative UTC offset
  245. test('should convert ISO date strings with negative UTC offset (-05:00) to Date objects', () => {
  246. const dateStringWithNegativeOffset = '2025-01-01T10:00:00-05:00';
  247. const input = {
  248. startTime: dateStringWithNegativeOffset,
  249. };
  250. const expected = {
  251. startTime: new Date(dateStringWithNegativeOffset),
  252. };
  253. const result = convertStringsToDates(input) as Record<
  254. string,
  255. DateConvertible
  256. >;
  257. expect(result.startTime).toBeInstanceOf(Date);
  258. if (result.startTime instanceof Date) {
  259. expect(result.startTime.toISOString()).toEqual(
  260. new Date(dateStringWithNegativeOffset).toISOString(),
  261. );
  262. }
  263. expect(result).toEqual(expected);
  264. });
  265. // Test case 12: Date string with zero UTC offset (+00:00)
  266. test('should convert ISO date strings with explicit zero UTC offset (+00:00) to Date objects', () => {
  267. const dateStringWithZeroOffset = '2025-03-15T12:00:00+00:00';
  268. const input = {
  269. zeroOffsetDate: dateStringWithZeroOffset,
  270. };
  271. const expected = {
  272. zeroOffsetDate: new Date(dateStringWithZeroOffset),
  273. };
  274. const result = convertStringsToDates(input) as Record<
  275. string,
  276. DateConvertible
  277. >;
  278. expect(result.zeroOffsetDate).toBeInstanceOf(Date);
  279. if (result.zeroOffsetDate instanceof Date) {
  280. expect(result.zeroOffsetDate.toISOString()).toEqual(
  281. new Date(dateStringWithZeroOffset).toISOString(),
  282. );
  283. }
  284. expect(result).toEqual(expected);
  285. });
  286. // Test case 13: Date string with milliseconds and UTC offset
  287. test('should convert ISO date strings with milliseconds and UTC offset to Date objects', () => {
  288. const dateStringWithMsAndOffset = '2025-10-20T23:59:59.999-07:00';
  289. const input = {
  290. detailedTime: dateStringWithMsAndOffset,
  291. };
  292. const expected = {
  293. detailedTime: new Date(dateStringWithMsAndOffset),
  294. };
  295. const result = convertStringsToDates(input) as Record<
  296. string,
  297. DateConvertible
  298. >;
  299. expect(result.detailedTime).toBeInstanceOf(Date);
  300. if (result.detailedTime instanceof Date) {
  301. expect(result.detailedTime.toISOString()).toEqual(
  302. new Date(dateStringWithMsAndOffset).toISOString(),
  303. );
  304. }
  305. expect(result).toEqual(expected);
  306. });
  307. // Test case 14: Should NOT convert strings that look like dates but are NOT ISO 8601 or missing timezone
  308. test('should NOT convert non-ISO 8601 date-like strings or strings missing timezone', () => {
  309. const nonIsoDate1 = '2025/06/12 14:00:00Z'; // Wrong separator
  310. const nonIsoDate2 = '2025-06-12T14:00:00'; // Missing timezone
  311. const nonIsoDate3 = 'June 12, 2025 14:00:00 GMT'; // Different format
  312. const nonIsoDate4 = '2025-06-12T14:00:00+0900'; // Missing colon in offset
  313. const nonIsoDate5 = '2025-06-12'; // Date only
  314. const input = {
  315. date1: nonIsoDate1,
  316. date2: nonIsoDate2,
  317. date3: nonIsoDate3,
  318. date4: nonIsoDate4,
  319. date5: nonIsoDate5,
  320. someOtherString: 'hello world',
  321. };
  322. // Deep copy to ensure comparison is accurate since the function modifies in place
  323. const expected = JSON.parse(JSON.stringify(input));
  324. const result = convertStringsToDates(input) as {
  325. date1: DateConvertible;
  326. date2: DateConvertible;
  327. date3: DateConvertible;
  328. date4: DateConvertible;
  329. date5: DateConvertible;
  330. someOtherString: string;
  331. };
  332. // Assert that they remain strings (or whatever their original type was)
  333. expect(typeof result.date1).toBe('string');
  334. expect(typeof result.date2).toBe('string');
  335. expect(typeof result.date3).toBe('string');
  336. expect(typeof result.date4).toBe('string');
  337. expect(typeof result.date5).toBe('string');
  338. expect(typeof result.someOtherString).toBe('string');
  339. // Ensure the entire object is unchanged for these properties
  340. expect(result.date1).toEqual(nonIsoDate1);
  341. expect(result.date2).toEqual(nonIsoDate2);
  342. expect(result.date3).toEqual(nonIsoDate3);
  343. expect(result.date4).toEqual(nonIsoDate4);
  344. expect(result.date5).toEqual(nonIsoDate5);
  345. expect(result.someOtherString).toEqual('hello world');
  346. // Finally, assert that the overall result is identical to the input for these non-matching strings
  347. expect(result).toEqual(expected);
  348. });
  349. describe('test circular reference occurrences', () => {
  350. // Test case 1: Circular references
  351. test('should handle circular references without crashing and preserve the cycle', () => {
  352. const dateString1 = '2023-02-20T12:30:00.000Z';
  353. const dateString2 = '2023-03-01T08:00:00.000Z';
  354. const dateString3 = '2023-04-05T14:15:00.000Z';
  355. const input: any = {
  356. data: {
  357. item1: {
  358. updatedAt: dateString1,
  359. value: 10,
  360. },
  361. item2: {
  362. nested1: {
  363. deletedAt: dateString2,
  364. isActive: false,
  365. nested2: {
  366. createdAt: dateString3,
  367. parent: null as any,
  368. },
  369. },
  370. anotherItem: {
  371. someValue: 42,
  372. lastSeen: '2023-11-01T12:00:00Z',
  373. },
  374. },
  375. },
  376. };
  377. // Create a circular reference
  378. input.data.item2.nested1.nested2.parent = input;
  379. const convertedOutput = convertStringsToDates(input) as {
  380. data: {
  381. item1: {
  382. updatedAt: DateConvertible;
  383. value: number;
  384. };
  385. item2: {
  386. nested1: {
  387. deletedAt: DateConvertible;
  388. isActive: boolean;
  389. nested2: {
  390. createdAt: DateConvertible;
  391. parent: any;
  392. };
  393. };
  394. anotherItem: {
  395. someValue: number;
  396. lastSeen: DateConvertible;
  397. };
  398. };
  399. };
  400. };
  401. // Expect the function not to have thrown an error
  402. expect(convertedOutput).toBeDefined();
  403. expect(convertedOutput).toBeInstanceOf(Object);
  404. // Check if circular reference is present
  405. expect(convertedOutput.data.item2.nested1.nested2.parent).toBe(input);
  406. // Check if the date conversion worked
  407. expect(convertedOutput.data.item1.updatedAt).toBeInstanceOf(Date);
  408. if (convertedOutput.data.item1.updatedAt instanceof Date) {
  409. expect(convertedOutput.data.item1.updatedAt.toISOString()).toBe(
  410. dateString1,
  411. );
  412. }
  413. expect(convertedOutput.data.item2.nested1.deletedAt).toBeInstanceOf(Date);
  414. if (convertedOutput.data.item2.nested1.deletedAt instanceof Date) {
  415. expect(convertedOutput.data.item2.nested1.deletedAt.toISOString()).toBe(
  416. dateString2,
  417. );
  418. }
  419. expect(
  420. convertedOutput.data.item2.nested1.nested2.createdAt,
  421. ).toBeInstanceOf(Date);
  422. if (
  423. convertedOutput.data.item2.nested1.nested2.createdAt instanceof Date
  424. ) {
  425. expect(
  426. convertedOutput.data.item2.nested1.nested2.createdAt.toISOString(),
  427. ).toBe(dateString3);
  428. }
  429. expect(convertedOutput.data.item2.anotherItem.lastSeen).toBeInstanceOf(
  430. Date,
  431. );
  432. if (convertedOutput.data.item2.anotherItem.lastSeen instanceof Date) {
  433. expect(
  434. convertedOutput.data.item2.anotherItem.lastSeen.toISOString(),
  435. ).toBe(new Date(input.data.item2.anotherItem.lastSeen).toISOString());
  436. }
  437. });
  438. // Test case 2: Direct self-reference
  439. test('should work when encountering direct self-references', () => {
  440. const obj: any = {};
  441. obj.self = obj;
  442. obj.createdAt = '2023-02-01T00:00:00Z';
  443. const converted = convertStringsToDates(obj) as Record<
  444. string,
  445. DateConvertible
  446. >;
  447. expect(converted).toBeDefined();
  448. expect(converted.self).toBe(obj);
  449. expect(converted.createdAt).toBeInstanceOf(Date);
  450. });
  451. });
  452. });