Skip to content

Commit

Permalink
feat: implement tests for TemperatureIterator
Browse files Browse the repository at this point in the history
  • Loading branch information
ReidyT committed Oct 25, 2024
1 parent b99ced1 commit 26e3f33
Show file tree
Hide file tree
Showing 2 changed files with 208 additions and 17 deletions.
177 changes: 177 additions & 0 deletions src/models/TemperatureIterator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { describe, expect, it } from 'vitest';

import { SIMULATION_SLIDING_WINDOW } from '@/config/simulation';
// Import your class
import { TemperatureRow } from '@/types/temperatures';
import { TimeUnit, TimeUnitType } from '@/types/time';
import { timeConversionFactors } from '@/utils/time';

import { TemperatureIterator } from './TemperatureIterator';

const createDummyTemperatures = (
count: number,
measurementFrequency: TimeUnitType,
): TemperatureRow[] =>
Array.from(
{
length:
(count * timeConversionFactors[TimeUnit.days]) /
timeConversionFactors[measurementFrequency],
},
(_, i) => ({
time: new Date(Date.UTC(2024, 0, i + 1, i)).toISOString(), // Create dates for each day or hour
temperature: 10 + i,
}),
);

describe('TemperatureIterator', () => {
it('should initialize correctly by hours', () => {
const temperatures = createDummyTemperatures(10, TimeUnit.hours);
expect(temperatures.length).toBe(240); // Should have more initially
const iterator = new TemperatureIterator({
temperatures,
measurementFrequency: TimeUnit.days,
});
expect(iterator.hasMore()).toBe(true); // Should have more initially
});

it('should initialize correctly by days', () => {
const temperatures = createDummyTemperatures(10, TimeUnit.days);
expect(temperatures.length).toBe(10); // Should have more initially
const iterator = new TemperatureIterator({
temperatures,
measurementFrequency: TimeUnit.days,
});
expect(iterator.hasMore()).toBe(true); // Should have more initially
});

it('should iterate through the temperatures with the correct sliding window', () => {
const temperatures = createDummyTemperatures(10, TimeUnit.days);

const iterator = new TemperatureIterator({
temperatures,
measurementFrequency: TimeUnit.days,
slidingWindow: { window: 3, unit: TimeUnit.days },
});

let window = iterator.getNext();
expect(window.temperatures).toEqual([10, 11, 12]);
expect(window.mean).toEqual(11);
expect(window.size).toEqual(3);
expect(window.totalCount).toEqual(10);

window = iterator.getNext();
expect(window.temperatures).toEqual([13, 14, 15]);
expect(window.mean).toEqual(14);

window = iterator.getNext();
expect(window.temperatures).toEqual([16, 17, 18]);
expect(window.mean).toEqual(17);

window = iterator.getNext();
expect(window.temperatures).toEqual([19]); // Last window might be smaller
expect(window.mean).toEqual(19);

expect(iterator.hasMore()).toBe(false); // Should have no more after iterating
});

it('should handle different measurement frequencies and sliding window units', () => {
const temperaturesHourly = createDummyTemperatures(5, TimeUnit.hours); // 5 days of hourly data
const iteratorHourly = new TemperatureIterator({
temperatures: temperaturesHourly,
measurementFrequency: TimeUnit.hours,
slidingWindow: { window: 2, unit: TimeUnit.days }, // 2-day window
});

const firstWindowHourly = iteratorHourly.getNext();
expect(firstWindowHourly.size).toBe(48); // 2 days * 24 hours/day

const temperaturesDaily = createDummyTemperatures(5, TimeUnit.days); // 5 days of daily data
const iteratorDaily = new TemperatureIterator({
temperatures: temperaturesDaily,
measurementFrequency: TimeUnit.days,
slidingWindow: { window: 2, unit: TimeUnit.days }, // 2-day window
});

const firstWindowDaily = iteratorDaily.getNext();
expect(firstWindowDaily.size).toBe(2); // 2 days
});

it('should not throw an error if the window size is greater than the number of temperatures', () => {
const temperatures = createDummyTemperatures(2, TimeUnit.days);
const iterator = new TemperatureIterator({
temperatures,
measurementFrequency: TimeUnit.days,
slidingWindow: { window: 3, unit: TimeUnit.days },
});

const firstWindowDaily = iterator.getNext(); // Get the first (and only) window
expect(firstWindowDaily.size).toBe(2); // 2 days
});

it('should throw an error if there are no temperatures', () => {
const temperatures = createDummyTemperatures(0, TimeUnit.days);
const iterator = new TemperatureIterator({
temperatures,
measurementFrequency: TimeUnit.days,
slidingWindow: { window: 3, unit: TimeUnit.days },
});

expect(() => iterator.getNext()).toThrowError(
'There is no more temperatures!',
);
});

it('should throw an error if there are no more temperatures', () => {
const temperatures = createDummyTemperatures(2, TimeUnit.days);
const iterator = new TemperatureIterator({
temperatures,
measurementFrequency: TimeUnit.days,
slidingWindow: { window: 2, unit: TimeUnit.days },
});

iterator.getNext(); // get first window

expect(() => iterator.getNext()).toThrowError(
'There is no more temperatures!',
);
});

it('should reset the iterator correctly', () => {
const temperatures = createDummyTemperatures(5, TimeUnit.days);
const iterator = new TemperatureIterator({
temperatures,
measurementFrequency: TimeUnit.days,
slidingWindow: { window: 2, unit: TimeUnit.days },
});

iterator.getNext(); // Move to the second window
iterator.reset(); // Reset the iterator

const resetWindow = iterator.getNext(); // Get the first window again
expect(resetWindow.temperatures).toEqual([10, 11]); // Should be back to the first window's data
});

it('should use the default sliding window if none is provided', () => {
const temperatures = createDummyTemperatures(10, TimeUnit.days);
const iterator = new TemperatureIterator({
temperatures,
measurementFrequency: TimeUnit.days,
});

const window = iterator.getNext();
expect(window.size).toBe(SIMULATION_SLIDING_WINDOW.window); // Should use the default window size

Check failure on line 163 in src/models/TemperatureIterator.test.ts

View workflow job for this annotation

GitHub Actions / cypress-run

src/models/TemperatureIterator.test.ts > TemperatureIterator > should use the default sliding window if none is provided

AssertionError: expected 10 to be 45 // Object.is equality - Expected + Received - 45 + 10 ❯ src/models/TemperatureIterator.test.ts:163:25
});

it('should handle edge cases with small datasets and large sliding windows', () => {
const temperatures = createDummyTemperatures(2, TimeUnit.days);
const iterator = new TemperatureIterator({
temperatures,
measurementFrequency: TimeUnit.days,
slidingWindow: { window: 5, unit: TimeUnit.days }, // Window larger than dataset
});

const window = iterator.getNext();
expect(window.temperatures).toEqual([10, 11]); // Should include all available data
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ export const initSlidingWindow = (idx: number): SlidingWindow => ({
});

const initSlidingWindowIdx = (slidingWindowSize: number): number => {
if (slidingWindowSize < 1) {
throw new Error('The sliding window size must be greater than 0.');
if (slidingWindowSize < 0) {
throw new Error('The sliding window size must be a positive number.');
}

// We initialize the index to - (sliding size - 1)
// We initialize the index to -slidingSize
// to ensure the next call to nextTemperature() starts at index 0.
return -slidingWindowSize - 1;
return -slidingWindowSize;
};

type TemperatureIteratorConstructor = {
Expand All @@ -43,6 +43,8 @@ export class TemperatureIterator {

private currentSlidingWindow: SlidingWindow;

private slidingWindowIdx: number;

/**
*
* @param temperatures - The complete array of loaded temperatures.
Expand Down Expand Up @@ -72,19 +74,22 @@ export class TemperatureIterator {
(slidingWindow.window * timeConversionFactors[slidingWindow.unit]) / // Convert window in hours
timeConversionFactors[measurementFrequency], // Convert window according to measurement frequency
);
this.currentSlidingWindow = initSlidingWindow(
initSlidingWindowIdx(this.slidingWindowSize),
);

this.slidingWindowIdx = initSlidingWindowIdx(this.slidingWindowSize);
this.currentSlidingWindow = initSlidingWindow(this.slidingWindowIdx);
}

hasMore(): boolean {
return this.currentSlidingWindow.idx + 1 < this.numberOfRows;
if (this.numberOfRows === 0) {
return false;
}

return this.slidingWindowIdx + 1 < this.numberOfRows;
}

reset(): void {
this.currentSlidingWindow = initSlidingWindow(
initSlidingWindowIdx(this.slidingWindowSize),
);
this.slidingWindowIdx = initSlidingWindowIdx(this.slidingWindowSize);
this.currentSlidingWindow = initSlidingWindow(this.slidingWindowIdx);
}

getNext(): SlidingWindow {
Expand All @@ -93,19 +98,28 @@ export class TemperatureIterator {
}

const { idx: prevIdx } = this.currentSlidingWindow;

const idx = Math.min(
prevIdx + this.slidingWindowSize + 1,
const currIdx = Math.min(
prevIdx + this.slidingWindowSize,
this.numberOfRows - 1,
);
const lastIdx = Math.min(idx + this.slidingWindowSize, this.numberOfRows);
const currentRows = this.temperatureRows.slice(idx, lastIdx);
const lastIdx = Math.min(
currIdx + this.slidingWindowSize,
this.numberOfRows,
);
const currentRows = this.temperatureRows.slice(currIdx, lastIdx);
const temperatures = currentRows.map((r) => r.temperature);
const total = temperatures.reduce((sum, currVal) => sum + currVal, 0);
const currentSize = temperatures.length;

// TODO: this is needed for now to work correctly!
// Without it, there is some weird case like:
// temperatures: [10, 11]
// getNext() -> [10, 11], expected [10, 11]
// getNext() -> [11], No more temperatures!
this.slidingWindowIdx = currIdx + 1;

this.currentSlidingWindow = {
idx,
idx: currIdx,
mean: Math.round(total / currentSize),
temperatures,
period: {
Expand Down

0 comments on commit 26e3f33

Please sign in to comment.