diff --git a/src/1byte.js b/src/1byte.js index 84e9e45..d3fb324 100644 --- a/src/1byte.js +++ b/src/1byte.js @@ -39,6 +39,7 @@ const KWH_PER_BYTE_FOR_DEVICES = 1.3e-10; class OneByte { constructor(options) { + this.allowRatings = false; this.options = options; this.KWH_PER_BYTE_FOR_NETWORK = KWH_PER_BYTE_FOR_NETWORK; diff --git a/src/co2.js b/src/co2.js index 7bddf6c..ff48b44 100644 --- a/src/co2.js +++ b/src/co2.js @@ -41,6 +41,7 @@ * @property {number} dataCenterCO2 - The CO2 estimate for data centers in grams * @property {number} consumerDeviceCO2 - The CO2 estimate for consumer devices in grams * @property {number} productionCO2 - The CO2 estimate for device production in grams + * @property {string} rating - The rating of the CO2 estimate based on the Sustainable Web Design Model * @property {number} total - The total CO2 estimate in grams */ @@ -54,6 +55,7 @@ * @property {number} 'consumerDeviceCO2 - subsequent' - The CO2 estimate for consumer devices in grams on subsequent visits * @property {number} 'productionCO2 - first' - The CO2 estimate for device production in grams on first visit * @property {number} 'productionCO2 - subsequent' - The CO2 estimate for device production in grams on subsequent visits + * @property {string} rating - The rating of the CO2 estimate based on the Sustainable Web Design Model * @property {number} total - The total CO2 estimate in grams */ @@ -81,8 +83,26 @@ class CO2 { ); } + if (options?.rating && typeof options.rating !== "boolean") { + throw new Error( + `The rating option must be a boolean. Please use true or false.\nSee https://developers.thegreenwebfoundation.org/co2js/options/ to learn more about the options available in CO2.js.` + ); + } + + // This flag checks to see if the model itself has a rating system. + const allowRatings = !!this.model.allowRatings; + /** @private */ this._segment = options?.results === "segment"; + // This flag is set by the user to enable the rating system. + this._rating = options?.rating === true; + + // The rating system is only supported in the Sustainable Web Design Model. + if (!allowRatings && this._rating) { + throw new Error( + `The rating system is not supported in the model you are using. Try using the Sustainable Web Design model instead.\nSee https://developers.thegreenwebfoundation.org/co2js/models/ to learn more about the models available in CO2.js.` + ); + } } /** @@ -95,7 +115,7 @@ class CO2 { * @return {number|CO2EstimateComponentsPerByte} the amount of CO2 in grammes or its separate components */ perByte(bytes, green = false) { - return this.model.perByte(bytes, green, this._segment); + return this.model.perByte(bytes, green, this._segment, this._rating); } /** @@ -109,7 +129,7 @@ class CO2 { */ perVisit(bytes, green = false) { if (this.model?.perVisit) { - return this.model.perVisit(bytes, green, this._segment); + return this.model.perVisit(bytes, green, this._segment, this._rating); } else { throw new Error( `The perVisit() method is not supported in the model you are using. Try using perByte() instead.\nSee https://developers.thegreenwebfoundation.org/co2js/methods/ to learn more about the methods available in CO2.js.` @@ -134,7 +154,13 @@ class CO2 { adjustments = parseOptions(options); } return { - co2: this.model.perByte(bytes, green, this._segment, adjustments), + co2: this.model.perByte( + bytes, + green, + this._segment, + this._rating, + adjustments + ), green, variables: { description: @@ -176,7 +202,13 @@ class CO2 { } return { - co2: this.model.perVisit(bytes, green, this._segment, adjustments), + co2: this.model.perVisit( + bytes, + green, + this._segment, + this._rating, + adjustments + ), green, variables: { description: diff --git a/src/co2.test.js b/src/co2.test.js index 72fb81e..6044812 100644 --- a/src/co2.test.js +++ b/src/co2.test.js @@ -211,6 +211,22 @@ describe("co2", () => { `The perVisit() method is not supported in the model you are using. Try using perByte() instead.\nSee https://developers.thegreenwebfoundation.org/co2js/methods/ to learn more about the methods available in CO2.js.` ); }); + + it("throws an error if using the rating system with OneByte", () => { + expect(() => { + co2 = new CO2({ model: "1byte", rating: true }); + }).toThrowError( + `The rating system is not supported in the model you are using. Try using the Sustainable Web Design model instead.\nSee https://developers.thegreenwebfoundation.org/co2js/models/ to learn more about the models available in CO2.js.` + ); + }); + + it("throws an error if the rating parameter is not a boolean", () => { + expect(() => { + co2 = new CO2({ rating: "false" }); + }).toThrowError( + `The rating option must be a boolean. Please use true or false.\nSee https://developers.thegreenwebfoundation.org/co2js/options/ to learn more about the options available in CO2.js.` + ); + }); }); // Test that grid intensity data can be imported and used @@ -832,4 +848,23 @@ describe("co2", () => { expect(co2Result["consumerDeviceCO2 - subsequent"]).toBe(0); }); }); + + describe("Returning SWD results with rating", () => { + const co2NoRating = new CO2(); + const co2Rating = new CO2({ rating: true }); + const co2RatingSegmented = new CO2({ rating: true, results: "segment" }); + + it("does not return a rating when rating is false", () => { + expect(co2NoRating.perVisit(MILLION)).not.toHaveProperty("rating"); + }); + + it("returns a rating when rating is true", () => { + expect(co2Rating.perVisit(MILLION)).toHaveProperty("rating"); + }); + + it("returns a rating when rating is true and results are segmented", () => { + expect(co2RatingSegmented.perByte(MILLION)).toHaveProperty("rating"); + expect(co2RatingSegmented.perByte(MILLION)).toHaveProperty("networkCO2"); + }); + }); }); diff --git a/src/constants/index.js b/src/constants/index.js index 0596b6f..f53735d 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -24,6 +24,15 @@ const FIRST_TIME_VIEWING_PERCENTAGE = 0.75; const RETURNING_VISITOR_PERCENTAGE = 0.25; const PERCENTAGE_OF_DATA_LOADED_ON_SUBSEQUENT_LOAD = 0.02; +const SWDMv3Ratings = { + fifthPercentile: 0.095, + tenthPercentile: 0.186, + twentiethPercentile: 0.341, + thirtiethPercentile: 0.493, + fortiethPercentile: 0.656, + fiftiethPercentile: 0.846, +}; + export { fileSize, KWH_PER_GB, @@ -36,4 +45,5 @@ export { FIRST_TIME_VIEWING_PERCENTAGE, RETURNING_VISITOR_PERCENTAGE, PERCENTAGE_OF_DATA_LOADED_ON_SUBSEQUENT_LOAD, + SWDMv3Ratings, }; diff --git a/src/helpers/index.js b/src/helpers/index.js index 1d0bc1a..aac11b5 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -17,6 +17,8 @@ import { const formatNumber = (num) => parseFloat(num.toFixed(2)); +const lessThanEqualTo = (num, limit) => num <= limit; + function parseOptions(options) { // CHeck that it is an object if (typeof options !== "object") { @@ -192,4 +194,4 @@ function getApiRequestHeaders(comment = "") { return { "User-Agent": `co2js/${process.env.CO2JS_VERSION} ${comment}` }; } -export { formatNumber, parseOptions, getApiRequestHeaders }; +export { formatNumber, parseOptions, getApiRequestHeaders, lessThanEqualTo }; diff --git a/src/sustainable-web-design.js b/src/sustainable-web-design.js index 613afd8..a7d5649 100644 --- a/src/sustainable-web-design.js +++ b/src/sustainable-web-design.js @@ -20,11 +20,22 @@ import { FIRST_TIME_VIEWING_PERCENTAGE, RETURNING_VISITOR_PERCENTAGE, PERCENTAGE_OF_DATA_LOADED_ON_SUBSEQUENT_LOAD, + SWDMv3Ratings, } from "./constants/index.js"; -import { formatNumber } from "./helpers/index.js"; +import { formatNumber, lessThanEqualTo } from "./helpers/index.js"; + +const { + fifthPercentile, + tenthPercentile, + twentiethPercentile, + thirtiethPercentile, + fortiethPercentile, + fiftiethPercentile, +} = SWDMv3Ratings; class SustainableWebDesign { constructor(options) { + this.allowRatings = true; this.options = options; } @@ -119,6 +130,7 @@ class SustainableWebDesign { * @param {number} bytes - the data transferred in bytes * @param {boolean} carbonIntensity - a boolean indicating whether the data center is green or not * @param {boolean} segmentResults - a boolean indicating whether to return the results broken down by component + * @param {boolean} ratingResults - a boolean indicating whether to return the rating based on the Sustainable Web Design Model * @param {object} options - an object containing the grid intensity and first/return visitor values * @return {number|object} the total number in grams of CO2 equivalent emissions, or an object containing the breakdown by component */ @@ -126,6 +138,7 @@ class SustainableWebDesign { bytes, carbonIntensity = false, segmentResults = false, + ratingResults = false, options = {} ) { if (bytes < 1) { @@ -153,10 +166,27 @@ class SustainableWebDesign { (prevValue, currentValue) => prevValue + currentValue ); + let rating = null; + if (ratingResults) { + rating = this.ratingScale(co2ValuesSum); + } + if (segmentResults) { + if (ratingResults) { + return { + ...co2ValuesbyComponent, + total: co2ValuesSum, + rating: rating, + }; + } + return { ...co2ValuesbyComponent, total: co2ValuesSum }; } + if (ratingResults) { + return { total: co2ValuesSum, rating: rating }; + } + return co2ValuesSum; } @@ -167,6 +197,7 @@ class SustainableWebDesign { * @param {number} bytes - the data transferred in bytes * @param {boolean} carbonIntensity - a boolean indicating whether the data center is green or not * @param {boolean} segmentResults - a boolean indicating whether to return the results broken down by component + * @param {boolean} ratingResults - a boolean indicating whether to return the rating based on the Sustainable Web Design Model * @param {object} options - an object containing the grid intensity and first/return visitor values * @return {number|object} the total number in grams of CO2 equivalent emissions, or an object containing the breakdown by component */ @@ -174,6 +205,7 @@ class SustainableWebDesign { bytes, carbonIntensity = false, segmentResults = false, + ratingResults = false, options = {} ) { const energyBycomponent = this.energyPerVisitByComponent(bytes, options); @@ -197,10 +229,26 @@ class SustainableWebDesign { (prevValue, currentValue) => prevValue + currentValue ); + let rating = null; + if (ratingResults) { + rating = this.ratingScale(co2ValuesSum); + } + if (segmentResults) { + if (ratingResults) { + return { + ...co2ValuesbyComponent, + total: co2ValuesSum, + rating: rating, + }; + } return { ...co2ValuesbyComponent, total: co2ValuesSum }; } + if (ratingResults) { + return { total: co2ValuesSum, rating: rating }; + } + // so we can return their sum return co2ValuesSum; } @@ -331,6 +379,30 @@ class SustainableWebDesign { productionEnergy: formatNumber(annualEnergy * PRODUCTION_ENERGY), }; } + + /** + * Determines the rating of a website's sustainability based on its CO2 emissions. + * + * @param {number} co2e - The CO2 emissions of the website in grams. + * @returns {string} The sustainability rating, ranging from "A+" (best) to "F" (worst). + */ + ratingScale(co2e) { + if (lessThanEqualTo(co2e, fifthPercentile)) { + return "A+"; + } else if (lessThanEqualTo(co2e, tenthPercentile)) { + return "A"; + } else if (lessThanEqualTo(co2e, twentiethPercentile)) { + return "B"; + } else if (lessThanEqualTo(co2e, thirtiethPercentile)) { + return "C"; + } else if (lessThanEqualTo(co2e, fortiethPercentile)) { + return "D"; + } else if (lessThanEqualTo(co2e, fiftiethPercentile)) { + return "E"; + } else { + return "F"; + } + } } export { SustainableWebDesign }; diff --git a/src/sustainable-web-design.test.js b/src/sustainable-web-design.test.js index 7c679a0..3ecc812 100644 --- a/src/sustainable-web-design.test.js +++ b/src/sustainable-web-design.test.js @@ -1,5 +1,15 @@ import SustainableWebDesign from "./sustainable-web-design.js"; import { MILLION, SWD } from "./constants/test-constants.js"; +import { SWDMv3Ratings } from "./constants/index.js"; + +const { + fifthPercentile, + tenthPercentile, + twentiethPercentile, + thirtiethPercentile, + fortiethPercentile, + fiftiethPercentile, +} = SWDMv3Ratings; describe("sustainable web design model", () => { const swd = new SustainableWebDesign(); @@ -113,4 +123,25 @@ describe("sustainable web design model", () => { }); }); }); + + describe("SWD Rating Scale", () => { + it("should return a string", () => { + expect(typeof swd.ratingScale(averageWebsiteInBytes)).toBe("string"); + }); + + it("should return a rating", () => { + // Check a 3MB file size + expect(swd.ratingScale(3000000)).toBe("F"); + }); + + it("returns ratings as expected", () => { + expect(swd.ratingScale(fifthPercentile)).toBe("A+"); + expect(swd.ratingScale(tenthPercentile)).toBe("A"); + expect(swd.ratingScale(twentiethPercentile)).toBe("B"); + expect(swd.ratingScale(thirtiethPercentile)).toBe("C"); + expect(swd.ratingScale(fortiethPercentile)).toBe("D"); + expect(swd.ratingScale(fiftiethPercentile)).toBe("E"); + expect(swd.ratingScale(0.9)).toBe("F"); + }); + }); });