Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 | import {
getDateTimeSortKey,
expandDicomDateTime,
getLatestInstanceDateTime,
getLatestInstanceDateTimeSortKey,
parseUTCOffset,
} from './latestInstanceDateTime';
describe('getLatestInstanceDateTime', () => {
test('uses the series date and time when they are the only pair', () => {
expect(getLatestInstanceDateTime({ SeriesDate: '20260817', SeriesTime: '093000' })).toEqual({
SeriesDate: '20260817',
SeriesTime: '093000',
});
});
test('reports an empty date and time rather than undefined', () => {
expect(getLatestInstanceDateTime({})).toEqual({ SeriesDate: '', SeriesTime: '' });
});
test('reads the lower camel case spelling of series metadata', () => {
expect(getLatestInstanceDateTime({ seriesDate: '20260817', seriesTime: '093000' })).toEqual({
SeriesDate: '20260817',
SeriesTime: '093000',
});
});
test('takes the latest date of all the attributes', () => {
expect(
getLatestInstanceDateTime({
SeriesDate: '20260817',
SeriesTime: '090000',
ContentDate: '20260819',
ContentTime: '140000',
})
).toEqual({ SeriesDate: '20260819', SeriesTime: '140000' });
});
// A report saved into an existing series keeps the series date and time of
// the day the series was first created, so only the instance level date and
// time say that it has just been added to.
test('prefers the instance date over an older series date', () => {
expect(
getLatestInstanceDateTime({
SeriesDate: '20260817',
SeriesTime: '090000',
InstanceCreationDate: '20260819',
InstanceCreationTime: '143000',
})
).toEqual({ SeriesDate: '20260819', SeriesTime: '143000' });
});
test('takes the latest time of the attributes carrying the winning date', () => {
expect(
getLatestInstanceDateTime({
SeriesDate: '20260819',
SeriesTime: '090000',
ContentDate: '20260819',
ContentTime: '140000',
StructureSetDate: '20260817',
StructureSetTime: '235959',
})
).toEqual({ SeriesDate: '20260819', SeriesTime: '140000' });
});
// Combining a date with the time of a different date would report a
// timestamp that never existed, and could order the display set anywhere
// within its day.
test('never takes a time from a date other than the winning one', () => {
expect(
getLatestInstanceDateTime({
SeriesDate: '20260818',
StructureSetDate: '20260817',
StructureSetTime: '090000',
})
).toEqual({ SeriesDate: '20260818', SeriesTime: '' });
});
test('uses the time of the winning date even when it comes from another attribute', () => {
expect(
getLatestInstanceDateTime({
SeriesDate: '20260817',
StructureSetDate: '20260817',
StructureSetTime: '090000',
})
).toEqual({ SeriesDate: '20260817', SeriesTime: '090000' });
});
// A SEG is authored after the series it segments, and carries that in its
// structure set date/time while the series date/time stay those of the
// images. The structure set pair is the one that says when the SEG was made.
test('prefers a later structure set date over the series date of a SEG', () => {
expect(
getLatestInstanceDateTime({
Modality: 'SEG',
SeriesDate: '20260817',
SeriesTime: '090000',
StructureSetDate: '20260819',
StructureSetTime: '143000',
})
).toEqual({ SeriesDate: '20260819', SeriesTime: '143000' });
});
// The winning date takes no time at all rather than the series time, which
// belongs to the day the images were acquired and not to the SEG.
test('leaves the time empty when the later SEG date carries none', () => {
expect(
getLatestInstanceDateTime({
Modality: 'SEG',
SeriesDate: '20260817',
SeriesTime: '090000',
StructureSetDate: '20260819',
})
).toEqual({ SeriesDate: '20260819', SeriesTime: '' });
});
test('ignores a time that has no date with it', () => {
expect(getLatestInstanceDateTime({ SeriesTime: '090000', ContentDate: '20260817' })).toEqual({
SeriesDate: '20260817',
SeriesTime: '',
});
});
// Some series level metadata carries a date already formatted for display.
// `19-Jan-2026` would read as `192026`, ordering by day of month and making
// two different months compare as equal, so it counts as no date at all.
test('ignores a date that is not a DICOM DA value', () => {
expect(getLatestInstanceDateTime({ SeriesDate: '19-Jan-2026' })).toEqual({
SeriesDate: '',
SeriesTime: '',
});
expect(getLatestInstanceDateTimeSortKey({ seriesDate: '05-Feb-2026' })).toBe('');
});
test('reads the dotted date of the retired DICOM form', () => {
expect(getLatestInstanceDateTime({ SeriesDate: '2026.08.17' })).toEqual({
SeriesDate: '2026.08.17',
SeriesTime: '',
});
});
test('ignores the study date, which every series in the study shares', () => {
expect(getLatestInstanceDateTime({ StudyDate: '20260819', StudyTime: '080000' })).toEqual({
SeriesDate: '',
SeriesTime: '',
});
});
test('splits a combined acquisition date time', () => {
expect(getLatestInstanceDateTime({ AcquisitionDateTime: '20260819143000.000000' })).toEqual({
SeriesDate: '20260819',
SeriesTime: '143000.000000',
});
});
// Two DT values that name the same instant in two different zones have to
// order as equal, whatever zone the viewer is in. Reading the offset as a
// part of the time gave `20260819+0500` five in the morning, and made the
// `05` of `202608191030-0500` the seconds.
test.each([
['a full value', '20260819100000.000000-0500', '20260819150000.000000+0000'],
['a value with no seconds', '202608191030-0500', '202608191530+0000'],
['a value with no time', '20260819+0000', '20260818200000-0400'],
['a value that crosses the day boundary', '20260819233000-0500', '20260820043000+0000'],
])('gives one sort key to %s in two zones', (_name, west, utc) => {
expect(getLatestInstanceDateTimeSortKey({ AcquisitionDateTime: west })).toBe(
getLatestInstanceDateTimeSortKey({ AcquisitionDateTime: utc })
);
});
// The equality above has to hold in the zone the viewer runs in, whichever
// one that is. It failed in CI and passed on a developer machine, because
// one of the two values declared the offset the machine was already in and
// came back with no time, while the other was moved and gained one.
test.each([[0], [-5 * 60], [5 * 60 + 30], [12 * 60]])(
'gives one sort key to one instant when the viewer is at %s minutes',
localOffsetMinutes => {
const west = expandDicomDateTime('20260819100000-0500', localOffsetMinutes);
const utc = expandDicomDateTime('20260819150000+0000', localOffsetMinutes);
const dateOnly = expandDicomDateTime('20260819+0000', localOffsetMinutes);
const midnight = expandDicomDateTime('20260819000000+0000', localOffsetMinutes);
expect(west).toEqual(utc);
expect(getDateTimeSortKey(dateOnly.SeriesDate, dateOnly.SeriesTime)).toBe(
getDateTimeSortKey(midnight.SeriesDate, midnight.SeriesTime)
);
}
);
test('leaves a combined date time that declares no offset exactly as it is', () => {
expect(getLatestInstanceDateTime({ AcquisitionDateTime: '20260819' })).toEqual({
SeriesDate: '20260819',
SeriesTime: '',
});
expect(getLatestInstanceDateTime({ AcquisitionDateTime: '202608191030' })).toEqual({
SeriesDate: '20260819',
SeriesTime: '1030',
});
});
test('takes the latest date time of an array of instances', () => {
const instances = [
{ ContentDate: '20260817', ContentTime: '090000' },
{ ContentDate: '20260819', ContentTime: '143000' },
{ ContentDate: '20260818', ContentTime: '235959' },
];
expect(getLatestInstanceDateTime(instances)).toEqual({
SeriesDate: '20260819',
SeriesTime: '143000',
});
});
});
describe('parseUTCOffset', () => {
test.each([
['+0000', 0],
['-0500', -300],
['+0930', 570],
['+1400', 840],
])('reads %s as %s minutes ahead of UTC', (value, expected) => {
expect(parseUTCOffset(value)).toBe(expected);
});
test.each([[undefined], [''], ['0500'], ['+05:00'], ['not an offset']])(
'reports %s as no offset',
value => {
expect(parseUTCOffset(value)).toBeUndefined();
}
);
});
/**
* The local offset is supplied to every case here, so the expected value does
* not depend on the zone the test runs in. `getLatestInstanceDateTime` supplies none
* and gets the viewer's own offset at that instant instead.
*/
describe('expandDicomDateTime', () => {
const utc = 0;
const newYork = -5 * 60;
const chicago = -6 * 60;
const adelaide = 9 * 60 + 30;
// Neither side is UTC here, so this pins the direction of both halves of the
// move: noon at -0400 is 16:00 UTC, and 16:00 UTC is 10:00 at -0600. The
// result is what a value carrying no offset at all would have to hold to name
// the same instant, because such a value is read as local.
test('moves a value between two offsets that are both behind UTC', () => {
expect(expandDicomDateTime('20260819120000-0400', chicago)).toEqual({
SeriesDate: '20260819',
SeriesTime: '100000',
});
// The same instant written in UTC, and the same answer.
expect(expandDicomDateTime('20260819160000+0000', chicago)).toEqual({
SeriesDate: '20260819',
SeriesTime: '100000',
});
// And the other way round, so a wrong sign cannot pass both.
expect(expandDicomDateTime('20260819100000-0600', -4 * 60)).toEqual({
SeriesDate: '20260819',
SeriesTime: '120000',
});
});
test('reads a DT that declares no offset exactly as it is', () => {
expect(expandDicomDateTime('20260819143000.000000', newYork)).toEqual({
SeriesDate: '20260819',
SeriesTime: '143000.000000',
});
});
test('keeps a DT that is already in the local offset', () => {
expect(expandDicomDateTime('20260819143000-0500', newYork)).toEqual({
SeriesDate: '20260819',
SeriesTime: '143000',
});
});
// A date alone gets the time of the start of that day even when it needs no
// move. Returning a date alone here would order the value before the same
// instant written out in another offset, which does get a time.
test('gives a date alone the start of the day even in the local offset', () => {
expect(expandDicomDateTime('20260819-0500', newYork)).toEqual({
SeriesDate: '20260819',
SeriesTime: '0000',
});
});
test('moves a DT to the local offset', () => {
expect(expandDicomDateTime('20260819150000+0000', newYork)).toEqual({
SeriesDate: '20260819',
SeriesTime: '100000',
});
expect(expandDicomDateTime('20260819100000-0500', utc)).toEqual({
SeriesDate: '20260819',
SeriesTime: '150000',
});
});
test('moves a DT across the day boundary', () => {
// Half past eleven at night in New York is half past four the next morning
// in UTC.
expect(expandDicomDateTime('20260819233000-0500', utc)).toEqual({
SeriesDate: '20260820',
SeriesTime: '043000',
});
// And half past midnight in UTC is still the previous evening there.
expect(expandDicomDateTime('20260820003000+0000', newYork)).toEqual({
SeriesDate: '20260819',
SeriesTime: '193000',
});
});
// A DT holding a date alone names the start of that day, which is the reading
// the move needs. The time it gains is a real one, so it is reported.
test('reads a date alone as the start of that day', () => {
expect(expandDicomDateTime('20260819+0000', newYork)).toEqual({
SeriesDate: '20260818',
SeriesTime: '1900',
});
});
test('moves by an offset that is not a whole number of hours', () => {
// 09:30 ahead of UTC, so 14:30 UTC is midnight the next day in Adelaide.
expect(expandDicomDateTime('20260819143000+0000', adelaide)).toEqual({
SeriesDate: '20260820',
SeriesTime: '000000',
});
});
test('keeps the seconds and the fraction of the source', () => {
expect(expandDicomDateTime('20260819150012.345678+0000', newYork)).toEqual({
SeriesDate: '20260819',
SeriesTime: '100012.345678',
});
});
// A DT truncated to the hour or the minute gains the minutes the move needs,
// and nothing below them.
test.each([
['20260819', '2026081900', '20260818', '1900'],
['20260819', '202608191030', '20260819', '0530'],
])('expands the truncated DT %s / %s', (_date, value, SeriesDate, SeriesTime) => {
expect(expandDicomDateTime(`${value}+0000`, newYork)).toEqual({ SeriesDate, SeriesTime });
});
test.each([[undefined], [''], ['2026'], ['202608'], ['not a date time']])(
'reports %s as no date time at all',
value => {
expect(expandDicomDateTime(value, utc)).toBeUndefined();
}
);
});
describe('getLatestInstanceDateTimeSortKey', () => {
const sortKey = source => getLatestInstanceDateTimeSortKey(source);
test('is empty with no date, which sorts as the oldest', () => {
expect(sortKey({})).toBe('');
expect(sortKey({ SeriesDate: '20260817' }) > '').toBe(true);
});
// HHMM and HHMMSS name the same instant, so they have to compare as equal.
test('pads times to a fixed width so equal instants compare as equal', () => {
expect(sortKey({ SeriesDate: '20260817', SeriesTime: '0930' })).toBe(
sortKey({ SeriesDate: '20260817', SeriesTime: '093000.0' })
);
});
test('sorts a date with no time before the timed values of that date', () => {
expect(
sortKey({ SeriesDate: '20260817' }) <
sortKey({ SeriesDate: '20260817', SeriesTime: '000000' })
).toBe(true);
});
test('orders by date first and then by time', () => {
expect(
sortKey({ SeriesDate: '20260817', SeriesTime: '235959' }) <
sortKey({ SeriesDate: '20260818', SeriesTime: '000000' })
).toBe(true);
expect(
sortKey({ SeriesDate: '20260818', SeriesTime: '093000' }) <
sortKey({ SeriesDate: '20260818', SeriesTime: '093001' })
).toBe(true);
});
});
|