From 90dabbd96bf2853af1bbbd4151374f3caac40276 Mon Sep 17 00:00:00 2001 From: Adel LAKHDAR <alakhdar@grandlyon.com> Date: Tue, 9 Apr 2024 08:06:34 +0000 Subject: [PATCH] feat(astuces) - Retravailler l'ordre d'affichage des astuces" --- src/models/ecogesture.model.ts | 2 + src/services/ecogesture.service.spec.ts | 62 ++++++++++++++++-- src/services/ecogesture.service.ts | 78 ++++++++++++---------- src/utils/date.ts | 29 ++++++++- tests/__mocks__/ecogesturesData.mock.ts | 86 +++++++++++++++++++++++++ 5 files changed, 219 insertions(+), 38 deletions(-) diff --git a/src/models/ecogesture.model.ts b/src/models/ecogesture.model.ts index d885795ce..caf04939d 100644 --- a/src/models/ecogesture.model.ts +++ b/src/models/ecogesture.model.ts @@ -25,4 +25,6 @@ export interface Ecogesture { _id: string _rev?: string _type?: string + /** computed value with efficiency, difficulty and current season */ + score?: number } diff --git a/src/services/ecogesture.service.spec.ts b/src/services/ecogesture.service.spec.ts index 29518141e..7661d0998 100644 --- a/src/services/ecogesture.service.spec.ts +++ b/src/services/ecogesture.service.spec.ts @@ -10,9 +10,11 @@ import { ecogesturesECSData, ecogesturesHeatingData, mockedEcogesturesData, + mockedEcogesturesSortedData, } from 'tests/__mocks__/ecogesturesData.mock' import { mockProfileEcogesture } from 'tests/__mocks__/profileEcogesture.mock' import { getError } from 'tests/__mocks__/testUtils' +import { SUMMER_MONTH_END, SUMMER_MONTH_START } from 'utils/date' import { hashFile } from 'utils/hash' import EcogestureService from './ecogesture.service' @@ -33,6 +35,13 @@ const mockQueryResultMockedEcogestures: QueryResult<Ecogesture[]> = { skip: 0, } +const mockQueryResultMockedSortedEcogestures: QueryResult<Ecogesture[]> = { + data: mockedEcogesturesSortedData, + bookmark: '', + next: false, + skip: 0, +} + const mockQueryResultEmpty: QueryResult<Ecogesture[]> = { data: [], bookmark: '', @@ -40,13 +49,19 @@ const mockQueryResultEmpty: QueryResult<Ecogesture[]> = { skip: 0, } +const currentMonth = new Date().getMonth() + 1 +const isSummerOrSpring = + currentMonth >= SUMMER_MONTH_START && currentMonth <= SUMMER_MONTH_END + describe('Ecogesture service', () => { const ecogestureService = new EcogestureService(mockClient) describe('getAllEcogestures', () => { it('should return all ecogestures', async () => { - mockClient.query.mockResolvedValueOnce(mockQueryResultMockedEcogestures) + mockClient.query.mockResolvedValueOnce( + mockQueryResultMockedSortedEcogestures + ) const result = await ecogestureService.getAllEcogestures() - expect(result).toEqual(mockedEcogesturesData) + expect(result).toEqual(mockedEcogesturesSortedData) }) it('should return empty array when no ecogestures stored', async () => { mockClient.query.mockResolvedValueOnce(mockQueryResultEmpty) @@ -181,8 +196,9 @@ describe('Ecogesture service', () => { expect(result.includes(BoilerEcogestureFalse[0])).toBeTruthy() }) }) + describe('getEcogestureListByProfile', () => { - it('should return ecogesture list according to profile ecogesture, sorted and filtered', async () => { + it('should return ecogesture list according to profile ecogesture, sorted and filtered, depending on the current season (WINTER/SUMMER)', async () => { const mockProfileEcogestureFull: ProfileEcogesture = { ...mockProfileEcogesture, equipments: [EquipmentType.WASHING_MACHINE, EquipmentType.DISHWASHER], @@ -193,10 +209,48 @@ describe('Ecogesture service', () => { const result = await ecogestureService.getEcogestureListByProfile( mockProfileEcogestureFull ) + expect(result.length).toBe(2) - expect(result[0]).toBe(mockedEcogesturesData[0]) + if (isSummerOrSpring) { + // eslint-disable-next-line jest/no-conditional-expect + expect(result[1]).toBe(mockedEcogesturesData[0]) + } else { + // eslint-disable-next-line jest/no-conditional-expect + expect(result[0]).toBe(mockedEcogesturesData[0]) + } }) }) + + describe('calculateScore', () => { + describe('during summer season', () => { + if (isSummerOrSpring) { + it('should return correct scores for each ecogesture', () => { + const scores = mockedEcogesturesData.map(ecogesture => + ecogestureService.calculateScore(ecogesture) + ) + + expect(scores[0]).toBe(0) + expect(scores[1]).toBe(8) + expect(scores[2]).toBe(1) + }) + } + }) + + describe('during winter season', () => { + if (!isSummerOrSpring) { + it('should return correct scores for each ecogesture', () => { + const scores = mockedEcogesturesData.map(ecogesture => + ecogestureService.calculateScore(ecogesture) + ) + + expect(scores[0]).toBe(8) + expect(scores[1]).toBe(0) + expect(scores[2]).toBe(1) + }) + } + }) + }) + describe('getEcogesturesByIds', () => { it('Should return corresponding ecogestures', async () => { const mockQueryResult: QueryResult<Ecogesture[]> = { diff --git a/src/services/ecogesture.service.ts b/src/services/ecogesture.service.ts index b3e6ddc5d..c03e9950f 100644 --- a/src/services/ecogesture.service.ts +++ b/src/services/ecogesture.service.ts @@ -12,6 +12,7 @@ import { } from 'enums' import { orderBy } from 'lodash' import { Ecogesture, ProfileEcogesture } from 'models' +import { getCurrentSeason, getOppositeSeason } from 'utils/date' import { logDuration } from 'utils/duration' import { hashFile } from 'utils/hash' import logApp from 'utils/logger' @@ -36,7 +37,7 @@ export default class EcogestureService { }> { const startTime = performance.now() const hashEcogestureType = hashFile(ecogestureData) - const ecogestures = await this.getAllEcogestures(undefined, true) + const ecogestures = await this.getAllEcogestures(true) if (!ecogestures || ecogestures?.length === 0) { // Populate data if none ecogesture exists @@ -115,18 +116,35 @@ export default class EcogestureService { } } - // TODO add default params - public async getAllEcogestures( - seasonFilter?: Season, - orderByID?: boolean - ): Promise<Ecogesture[]> { + /** + * Calculate Ecogesture score from efficiency and difficulty + * + * Base score = efficiency * 2 - difficulty + * - If current season, Score + 1 + * - If opposite season, Score = 0 + * - If no season, base score + */ + public calculateScore(ecogesture: Ecogesture): number { + const score = ecogesture.efficiency * 2 - ecogesture.difficulty + + const currentSeason = getCurrentSeason() + const oppositeSeason = getOppositeSeason(currentSeason) + + if (ecogesture.season !== Season.NONE) { + if (ecogesture.season === currentSeason) { + return score + 1 + } else if (ecogesture.season === oppositeSeason) { + return 0 + } + } + + return score + } + + public async getAllEcogestures(orderByID?: boolean): Promise<Ecogesture[]> { let query: QueryDefinition = Q(ECOGESTURE_DOCTYPE) - if (seasonFilter && seasonFilter !== Season.NONE) { - query = query - .where({ season: { $ne: seasonFilter } }) - .indexFields(['season']) - .sortBy([{ season: 'desc' }]) - } else if (orderByID) { + + if (orderByID) { query = query .where({}) .indexFields(['_id']) @@ -141,17 +159,13 @@ export default class EcogestureService { const { data: ecogestures }: QueryResult<Ecogesture[]> = await this._client.query(query) - if (seasonFilter && seasonFilter !== Season.NONE) { - const { data: ecogesturesWithSeason }: QueryResult<Ecogesture[]> = - await this._client.query( - Q(ECOGESTURE_DOCTYPE) - .where({ season: { $eq: seasonFilter } }) - .indexFields(['season']) - .sortBy([{ season: 'asc' }]) - ) - return [...ecogesturesWithSeason, ...ecogestures] + for (const ecogesture of ecogestures) { + const score = this.calculateScore(ecogesture) + ecogesture.score = score } - return ecogestures + + const sortedByScoreDesc = orderBy(ecogestures, 'score', 'desc') + return sortedByScoreDesc } /** @@ -269,14 +283,17 @@ export default class EcogestureService { profileEcogesture: ProfileEcogesture ): Promise<Ecogesture[]> { const ecogestureList = await this.getAllEcogestures() + const filteredByUsage = this.filterByUsage( ecogestureList, profileEcogesture ) + const filteredByEquipment = this.filterByEquipment( filteredByUsage, profileEcogesture ) + const filteredFlaggedEcogesture = filteredByEquipment.filter( ecogesture => (ecogesture.objective === false && @@ -284,19 +301,14 @@ export default class EcogestureService { ecogesture.viewedInSelection === false) || ecogesture.viewedInSelection === true ) - const sortedByDifficultyAndEfficiency = orderBy( + + const sortedByScoreDesc = orderBy( filteredFlaggedEcogesture, - [ - ecogesture => { - return ecogesture.difficulty - }, - ecogesture => { - return ecogesture.efficiency - }, - ], - ['asc', 'desc'] + 'score', + 'desc' ) - return sortedByDifficultyAndEfficiency + + return sortedByScoreDesc } /** diff --git a/src/utils/date.ts b/src/utils/date.ts index e474fdd49..764ea5c0e 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -1,8 +1,11 @@ -import { FluidType, TimeStep } from 'enums' +import { FluidType, Season, TimeStep } from 'enums' import { DateTime } from 'luxon' import { Dataload } from 'models' import { getMonthNameWithPrep } from './utils' +export const SUMMER_MONTH_START = 3 +export const SUMMER_MONTH_END = 8 + export function compareDates(dateA: DateTime, dateB: DateTime) { return dateA < dateB ? -1 : 1 } @@ -144,3 +147,27 @@ export const getActualAnalysisDate = (): DateTime => { return now.set({ day: 3, month: now.month }) } } + +/** + * Determines the current season based on the month of the year. + * @returns {Season} Returns Season.SUMMER if the current month is between March and August, and Season.WINTER for the rest of the months. + */ +export function getCurrentSeason(): Season { + const currentMonth = new Date().getMonth() + 1 + + if (currentMonth >= SUMMER_MONTH_START && currentMonth <= SUMMER_MONTH_END) { + return Season.SUMMER + } else { + return Season.WINTER + } +} + +export function getOppositeSeason(currentSeason: Season): Season { + if (currentSeason === Season.WINTER) { + return Season.SUMMER + } else if (currentSeason === Season.SUMMER) { + return Season.WINTER + } else { + throw new Error('Invalid current season.') + } +} diff --git a/tests/__mocks__/ecogesturesData.mock.ts b/tests/__mocks__/ecogesturesData.mock.ts index c7a61cc90..de6e545c8 100644 --- a/tests/__mocks__/ecogesturesData.mock.ts +++ b/tests/__mocks__/ecogesturesData.mock.ts @@ -89,6 +89,92 @@ export const mockedEcogesturesData: Ecogesture[] = [ }, ] +export const mockedEcogesturesSortedData: Ecogesture[] = [ + { + fluidTypes: [FluidType.ELECTRICITY], + id: 'ECOGESTURE0002', + longDescription: + "Cela permet de garder la fraîcheur à l'intérieur. Le climatiseur n'est pas là pour refroidir la rue mais bien la pièce.", + longName: 'Je ferme mes fenêtres quand la climatisation est en marche', + shortName: 'Portique thermique', + usage: Usage.AIR_CONDITIONING, + impactLevel: 8, + efficiency: 4, + difficulty: 1, + room: [Room.ALL], + season: Season.SUMMER, + equipment: true, + equipmentType: [EquipmentType.AIR_CONDITIONING], + equipmentInstallation: true, + investment: null, + action: false, + actionName: null, + actionDuration: 3, + doing: false, + objective: false, + viewedInSelection: false, + _id: 'ECOGESTURE0002', + _rev: '19-9b604bcf7e55f8650e2be4f676fddaf0', + _type: 'com.grandlyon.ecolyo.ecogesture', + }, + { + fluidTypes: [FluidType.ELECTRICITY], + id: 'ECOGESTURE0035', + longDescription: + 'Réglez votre climatisation au plus bas à 26 °C et veillez à ce qu’il n’y ait jamais plus de 5 à 7 °C de différence entre l’intérieur et l’extérieur. Attention aux grands écarts de température qui peuvent provoquer des chocs thermiques.', + longName: + 'Je règle ma climatisation au plus bas à 26°C en veillant à ce qu’il n’y ait pas jamais plus de 5°C à 7°C de différence entre l’intérieur et l’extérieur.', + shortName: "La Juste Clim'", + usage: Usage.AIR_CONDITIONING, + impactLevel: 8, + efficiency: 4, + difficulty: 2, + room: [Room.ALL], + season: Season.SUMMER, + equipment: true, + equipmentType: [EquipmentType.AIR_CONDITIONING], + equipmentInstallation: true, + investment: null, + action: false, + actionName: null, + actionDuration: 3, + doing: false, + objective: false, + viewedInSelection: false, + _id: 'ECOGESTURE0035', + _rev: '17-666648f24f6c797443470a54785322c5', + _type: 'com.grandlyon.ecolyo.ecogesture', + }, + { + fluidTypes: [FluidType.ELECTRICITY], + id: 'ECOGESTURE0034', + longDescription: + "Cela permet d'évite des consommations inutiles. Le froid ne restera pas dans la pièce. Donc il est préférable d'allumer le ventilateur ou climatiseur seulement quand des personnes sont présentes dans la pièce.", + longName: + 'Je ne fais pas fonctionner mon ventilateur ou la climatisation dans les pièces non occupées', + shortName: 'Bulles-à -part', + usage: Usage.AIR_CONDITIONING, + impactLevel: 8, + efficiency: 4, + difficulty: 2, + room: [Room.ALL], + season: Season.SUMMER, + equipment: true, + equipmentType: [EquipmentType.AIR_CONDITIONING, EquipmentType.FAN], + equipmentInstallation: true, + investment: null, + action: false, + actionName: null, + actionDuration: 3, + doing: false, + objective: false, + viewedInSelection: false, + _id: 'ECOGESTURE0034', + _rev: '25-75810c7b375dcf2742f0b225fda7c6d6', + _type: 'com.grandlyon.ecolyo.ecogesture', + }, +] + export const ecogesturesHeatingData: Ecogesture[] = [ { fluidTypes: [FluidType.ELECTRICITY], -- GitLab