diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index f06235c..0000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index b368dda..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "env": { - "es6": true, - "node": true, - "jest": true, - "browser": true - }, - "globals": {}, - "plugins": [], - "overrides": [], - "extends": ["eslint:recommended"], - "rules": { - "arrow-spacing": ["error", { "before": true, "after": true }], - "block-spacing": ["error", "always"], - "brace-style": ["error", "1tbs", { "allowSingleLine": true }], - "camelcase": ["error", { - "allow": ["^UNSAFE_"], - "properties": "never", - "ignoreGlobals": true - }], - "comma-dangle": ["error", { - "arrays": "always-multiline", - "objects": "always-multiline", - "imports": "never", - "exports": "never", - "functions": "never" - }], - "comma-spacing": ["error", { "before": false, "after": true }], - "eol-last": "error", - "eqeqeq": ["error", "always", { "null": "ignore" }], - "func-call-spacing": ["error", "never"], - "indent": [ - "error", - 2, - { - "MemberExpression": 1, - "FunctionDeclaration": { - "body": 1, - "parameters": 2 - }, - "SwitchCase": 1 - } - ], - "key-spacing": ["error", { "beforeColon": false, "afterColon": true }], - "keyword-spacing": ["error", { "before": true, "after": true }], - "lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }], - "max-len": [ - "error", - { - "code": 120, - "ignoreTrailingComments": true, - "ignoreComments": true, - "ignoreUrls": true - } - ], - "max-lines": [ - "error", - { - "max": 360, - "skipBlankLines": true, - "skipComments": false - } - ], - "max-lines-per-function": [ - "error", - { - "max": 250, - "skipBlankLines": true - } - ], - "max-params": ["error", 4], - "no-array-constructor": "error", - "no-mixed-spaces-and-tabs": "error", - "no-multi-spaces": "error", - "no-multi-str": "error", - "no-multiple-empty-lines": [ - "error", - { - "max": 1, - "maxEOF": 0 - } - ], - "no-restricted-syntax": [ - "error", - "WithStatement", - "BinaryExpression[operator='in']" - ], - "no-trailing-spaces": "error", - "no-use-before-define": [ - "error", - { - "functions": true, - "classes": true, - "variables": false - } - ], - "no-var": "warn", - "object-curly-spacing": ["error", "always"], - "padded-blocks": [ - "error", - { - "blocks": "never", - "switches": "never", - "classes": "never" - } - ], - "quotes": ["error", "single"], - "space-before-blocks": ["error", "always"], - "space-before-function-paren": ["error", "always"], - "space-infix-ops": "error", - "space-unary-ops": ["error", { "words": true, "nonwords": false }], - "space-in-parens": ["error", "never"], - "semi": ["error", "never"] - } -} diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 0166bd8..51fb187 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -8,39 +8,31 @@ on: [push, pull_request] jobs: test: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest strategy: matrix: - node_version: [14.x, 16.x, 18.x, 19.x] + deno-version: [1.44.4] steps: - - uses: actions/checkout@v3 - - - name: setup Node.js v${{ matrix.node_version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node_version }} - - - name: run npm scripts - run: | - npm install - npm run lint - npm run build --if-present - npm run test - - - name: Coveralls GitHub Action - uses: coverallsapp/github-action@v1.1.2 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: cache node modules - uses: actions/cache@v3 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - + - name: Git Checkout Deno Module + uses: actions/checkout@v4 + - name: Use Deno Version ${{ matrix.deno-version }} + uses: denoland/setup-deno@v1 + with: + deno-version: ${{ matrix.deno-version }} + - name: format check + run: deno fmt --check mod.ts scripts/* tests/* + - name: run linter + run: deno lint mod.ts utils/* scripts/* tests/* + - name: run test + run: deno test --allow-all --coverage=cov/ + + - name: Generate coverage report + run: deno coverage --lcov cov > cov.lcov + + - name: Upload coverage to Coveralls.io + uses: coverallsapp/github-action@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: cov.lcov diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2124bd6..102df5a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,14 +1,3 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# name: "CodeQL" on: @@ -38,7 +27,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..b1c0ff2 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,16 @@ +name: publish + +on: + push: + branches: + - main + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # The OIDC ID token is used for authentication with JSR. + steps: + - uses: actions/checkout@v4 + - run: npx jsr publish diff --git a/.gitignore b/.gitignore index 47be9c0..3891bf2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,13 +5,18 @@ logs # Runtime data *.pid +*.lock *.seed node_modules coverage +coverage.lcov .nyc_output yarn.lock -coverage.lcov -package-lock.json pnpm-lock.yaml +package-lock.json + +npm +cov +cov.lcov diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 7f1eb85..0000000 --- a/.npmignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules -coverage -.github -pnpm-lock.yaml -examples -build.js -build.test.js -reset.js diff --git a/README.md b/README.md index 5e5a82f..865b411 100644 --- a/README.md +++ b/README.md @@ -1,115 +1,104 @@ # average-rating -Calculate average and scoring based on Wilson Score Equation -[![NPM](https://badge.fury.io/js/average-rating.svg)](https://badge.fury.io/js/average-rating) -![CI test](https://github.com/ndaidong/average-rating/workflows/ci-test/badge.svg) -[![Coverage Status](https://coveralls.io/repos/github/ndaidong/average-rating/badge.svg)](https://coveralls.io/github/ndaidong/average-rating) +Calculate average and scoring based on Wilson Score Equation + ![CodeQL](https://github.com/ndaidong/average-rating/workflows/CodeQL/badge.svg) +[![CI test](https://github.com/ndaidong/average-rating/workflows/ci-test/badge.svg)](https://github.com/ndaidong/average-rating/actions) +[![Coverage Status](https://coveralls.io/repos/github/ndaidong/average-rating/badge.svg)](https://coveralls.io/github/ndaidong/average-rating) +[![NPM](https://img.shields.io/npm/v/%40ndaidong%2Faverage-rating?color=32bb24)](https://www.npmjs.com/package/@ndaidong/average-rating) +[![JSR](https://jsr.io/badges/@ndaidong/average-rating?color=32bb24)](https://jsr.io/@ndaidong/average-rating) ![Google app on Google Play](https://i.imgur.com/XKEEpdb.png) -## Install & Usage +## Setup & Usage -# Node.js +### Deno -```bash -npm i average-rating +https://jsr.io/@ndaidong/average-rating -# pnpm -pnpm i average-rating +```sh +deno add @ndaidong/average-rating -# yarn -yarn add average-rating +# npm (use any of npx, yarn dlx, pnpm dlx, or bunx) +npx jsr add @ndaidong/average-rating ``` ```ts // es6 module -import { - score, - rate, - average -} from 'average-rating' +import { average, rate, score } from "@ndaidong/average-rating"; // CommonJS const { score, rate, - average -} = require('average-rating') + average, +} = require("@ndaidong/average-rating"); -// or specify exactly path to CommonJS variant -const { - score, - rate, - average -} = require('average-rating/dist/cjs/average-rating.js') +score(80, 20); // => 0.71 +rate([134055, 57472, 143135, 365957, 1448459]); // => 0.84 +average([134055, 57472, 143135, 365957, 1448459]); // => 4.4 +``` -score(80, 20) // => 0.71 -average([134055, 57472, 143135, 365957, 1448459]) // => 4.4 -rate([134055, 57472, 143135, 365957, 1448459]) // => 0.84 +You can use JSR packages without an install step using `jsr:` specifiers: + +```ts +import { average } from "jsr:@ndaidong/average-rating"; + +average([134055, 57472, 143135, 365957, 1448459]); // => 4.4 ``` -### Deno +You can also use `npm:` specifiers as before: ```ts -// deno > 1.28 -import { - score, - rate, - average -} from 'npm:average-rating' +import { average } from "npm:@ndaidong/average-rating"; -// deno < 1.28 -import { - score, - rate, - average -} from 'https://esm.sh/average-rating' +average([134055, 57472, 143135, 365957, 1448459]); // => 4.4 ``` -### CDN +Or import from esm.sh -- ES6 Module: [average-rating.esm.js](https://unpkg.com/average-rating/dist/average-rating.esm.js) -- CommonJS: [average-rating.js](https://unpkg.com/average-rating/dist/cjs/average-rating.js) -- For old browsers: [average-rating.min.js](https://unpkg.com/average-rating/dist/average-rating.min.js) +```ts +import { average } from "https://esm.sh/@ndaidong/average-rating"; -Currently ECMAScript modules work fine on almost browsers: +average([134055, 57472, 143135, 365957, 1448459]); // => 4.4 +``` -```html - +```ts +import { average } from "@ndaidong/average-rating"; + +average([134055, 57472, 143135, 365957, 1448459]); // => 4.4 ``` -With outdated browsers, we can use traditional way: +You can also use CJS style: -```html - +```ts +const { average } = require("@ndaidong/average-rating"); - ``` @@ -119,23 +108,51 @@ const { Return a value from 0 to 1. -Used for the systems of Positive/Negative rating, such as the videos on YouTube, the answers on StackOverflow, etc. In which, each of item can be voted as good or bad, like or dislike or something like that. +Used for the systems of Positive/Negative rating, such as the videos on YouTube, +the answers on StackOverflow, etc. In which, each of item can be voted as good +or bad, like or dislike or something like that. + +For example, here we calculate score of a blog post with 80 likes and 20 +dislikes: + +```ts +import { score } from "@ndaidong/average-rating"; + +score(80, 20); // => 0.71 +``` ### .rate(Array ratings) Return a value from 0 to 1. -Used for the systems of 5 rating levels, such as the applications on Google Play store, the books on Amazon, etc. In which, each of item can be voted as one of value in the range of 1 to 5 stars. +Used for the systems of 5 rating levels, such as the applications on Google Play +store, the books on Amazon, etc. In which, each of item can be voted as one of +value in the range of 1 to 5 stars. + +For example, here we calculate rating value of a product with: + +- 134,055 rates of 1 star +- 57,472 rates of 2 stars +- 143,135 rates of 3 stars +- 365,957 rates of 4 stars +- 1,448,459 rates of 5 stars + +```ts +import { rate } from "@ndaidong/average-rating"; + +rate([134055, 57472, 143135, 365957, 1448459]); // => 0.84 +``` ##### Update -- Since v1.1.5, this `rate` method accepts custom range of ratings. 5 or more values are OK. +- Since v1.1.5, this `rate` method accepts custom range of ratings. 5 or more + values are OK. -```js -const input = [3, 4, 2, 6, 12, 46, 134, 213, 116, 91, 45, 15, 58, 96, 1654] // 15 values -rate(input) // => 0.85 +```ts +const input = [3, 4, 2, 6, 12, 46, 134, 213, 116, 91, 45, 15, 58, 96, 1654]; // 15 values +rate(input); // => 0.85 -rate([3, 4, 2, 6, 12, 46, 134, 213, 116, 91]) // => 0.74 +rate([3, 4, 2, 6, 12, 46, 134, 213, 116, 91]); // => 0.74 ``` ### .average(Array ratings) @@ -144,17 +161,28 @@ Return a value from 0 to 5. Calculate normal average value for the systems of 5 rating levels. +## Development -## Test +Since v3.x.x, we switched to [Deno](https://docs.deno.com/runtime/manual/) +platform, and use [DNT](https://github.com/denoland/dnt) to build Node.js +packages. ```bash git clone https://github.com/ndaidong/average-rating.git cd average-rating -npm install -npm test -``` +# test +deno test + +# build npm packages +deno task build + +cd npm +node test_runner.js +``` # License The MIT License (MIT) + +--- diff --git a/build.js b/build.js deleted file mode 100644 index 189a5a0..0000000 --- a/build.js +++ /dev/null @@ -1,80 +0,0 @@ -/** - * build.js - * @ndaidong -**/ - -import { readFileSync, writeFileSync } from 'fs' -import { execSync } from 'child_process' - -import { buildSync } from 'esbuild' - -const pkg = JSON.parse(readFileSync('./package.json')) - -execSync('rm -rf dist') -execSync('mkdir dist') - -const buildTime = (new Date()).toISOString() -const comment = [ - `// ${pkg.name}@${pkg.version}, by ${pkg.author}`, - `built with esbuild at ${buildTime}`, - `published under ${pkg.license} license`, -].join(' - ') - -const baseOpt = { - entryPoints: ['src/main.js'], - bundle: true, - charset: 'utf8', - target: ['es2020', 'node14'], - minify: false, - write: true, -} - -const esmVersion = { - ...baseOpt, - platform: 'neutral', - format: 'esm', - mainFields: ['module'], - outfile: 'dist/average-rating.esm.js', - banner: { - js: comment, - }, -} -buildSync(esmVersion) - -const cjsVersion = { - ...baseOpt, - platform: 'node', - format: 'cjs', - mainFields: ['main'], - outfile: 'dist/cjs/average-rating.js', - banner: { - js: comment, - }, -} -buildSync(cjsVersion) - -const cjspkg = { - name: pkg.name, - version: pkg.version, - main: './average-rating.js', -} -writeFileSync( - 'dist/cjs/package.json', - JSON.stringify(cjspkg, null, ' '), - 'utf8' -) - -const iifeVersion = { - ...baseOpt, - platform: 'browser', - format: 'iife', - mainFields: ['browser'], - target: ['es2020'], - globalName: 'AverageRating', - outfile: 'dist/average-rating.min.js', - minify: true, - banner: { - js: comment, - }, -} -buildSync(iifeVersion) diff --git a/build.test.js b/build.test.js deleted file mode 100644 index 1179ca2..0000000 --- a/build.test.js +++ /dev/null @@ -1,41 +0,0 @@ -// release.test - -/* eslint-env jest */ - -import { - existsSync, - readFileSync -} from 'fs' - -const pkg = JSON.parse(readFileSync('./package.json')) - -const esmFile = `${pkg.name}.esm.js` -const cjsFile = `cjs/${pkg.name}.js` -const minFile = `${pkg.name}.min.js` - -const runtest = (fname) => { - const fpath = `./dist/${fname}` - describe(`Validate ${fname} version output`, () => { - test(`Check if ${fpath} file created`, () => { - expect(existsSync(fpath)).toBeTruthy() - }) - const constent = readFileSync(fpath, 'utf8') - const lines = constent.split('\n') - test('Check if file meta contains package info', () => { - expect(lines[0].includes(`${pkg.name}@${pkg.version}`)).toBeTruthy() - expect(lines[0].includes(pkg.author)).toBeTruthy() - expect(lines[0].includes(pkg.license)).toBeTruthy() - }) - }) - return fpath -} - -const arr = [ - esmFile, - cjsFile, - minFile, -] - -arr.forEach((fname) => { - runtest(fname) -}) diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..dd76e04 --- /dev/null +++ b/deno.json @@ -0,0 +1,31 @@ +{ + "name": "@ndaidong/average-rating", + "version": "3.0.0-rc1", + "description": "Calculate average score and rating based on Wilson Score Equation", + "homepage": "https://github.com/ndaidong/average-rating", + "repository": { + "type": "git", + "url": "git+https://github.com/ndaidong/average-rating.git" + }, + "author": "@ndaidong", + "license": "MIT", + "tasks": { + "build": "deno run -A ./scripts/build_npm.ts" + }, + "imports": { + "assert": "https://deno.land/std@0.224.0/assert/mod.ts", + "@deno/dnt": "jsr:@deno/dnt@^0.41.2" + }, + "exports": "./mod.ts", + "test": { + "include": ["tests"], + "exclude": [] + }, + "publish": { + "include": [ + "LICENSE", + "README.md", + "mod.ts" + ] + } +} diff --git a/dist/average-rating.esm.js b/dist/average-rating.esm.js deleted file mode 100644 index 578f3e2..0000000 --- a/dist/average-rating.esm.js +++ /dev/null @@ -1,44 +0,0 @@ -// average-rating@2.0.4, by @ndaidong - built with esbuild at 2023-03-12T04:52:52.906Z - published under MIT license - -// src/main.js -var score = (p = 0, n = 0) => { - if (p === 0 && n === 0) { - return 0; - } - const r = ((p + 1.9208) / (p + n) - 1.96 * Math.sqrt(p * n / (p + n) + 0.9604) / (p + n)) / (1 + 3.8416 / (p + n)); - return Number(r.toFixed(2)); -}; -var rate = (rating = []) => { - const size = rating.length; - let n = rating[0]; - let p = rating[size - 1]; - const step = (1 / (size - 1)).toFixed(2); - const totalStep = size - 1; - for (let i = 1; i < totalStep; i++) { - const ep = (step * i).toFixed(2); - p += rating[i] * ep; - n += rating[totalStep - i] * ep; - } - return score(p, n); -}; -var average = (rating = []) => { - const total = rating.reduce((prev, current) => { - return prev + current; - }, 0); - if (total === 0) { - return 0; - } - let sum = 0; - let k = 1; - rating.forEach((item) => { - sum += item * k; - k++; - }); - const r = sum / total; - return Number(r.toFixed(1)); -}; -export { - average, - rate, - score -}; diff --git a/dist/average-rating.min.js b/dist/average-rating.min.js deleted file mode 100644 index 79500ae..0000000 --- a/dist/average-rating.min.js +++ /dev/null @@ -1,2 +0,0 @@ -// average-rating@2.0.4, by @ndaidong - built with esbuild at 2023-03-12T04:52:52.906Z - published under MIT license -var AverageRating=(()=>{var l=Object.defineProperty;var d=Object.getOwnPropertyDescriptor;var f=Object.getOwnPropertyNames;var F=Object.prototype.hasOwnProperty;var h=(t,e)=>{for(var o in e)l(t,o,{get:e[o],enumerable:!0})},i=(t,e,o,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of f(e))!F.call(t,r)&&r!==o&&l(t,r,{get:()=>e[r],enumerable:!(s=d(e,r))||s.enumerable});return t};var m=t=>i(l({},"__esModule",{value:!0}),t);var N={};h(N,{average:()=>b,rate:()=>a,score:()=>x});var x=(t=0,e=0)=>{if(t===0&&e===0)return 0;let o=((t+1.9208)/(t+e)-1.96*Math.sqrt(t*e/(t+e)+.9604)/(t+e))/(1+3.8416/(t+e));return Number(o.toFixed(2))},a=(t=[])=>{let e=t.length,o=t[0],s=t[e-1],r=(1/(e-1)).toFixed(2),u=e-1;for(let c=1;c{let e=t.reduce((u,c)=>u+c,0);if(e===0)return 0;let o=0,s=1;t.forEach(u=>{o+=u*s,s++});let r=o/e;return Number(r.toFixed(1))};return m(N);})(); diff --git a/dist/cjs/average-rating.js b/dist/cjs/average-rating.js deleted file mode 100644 index 3379a5c..0000000 --- a/dist/cjs/average-rating.js +++ /dev/null @@ -1,69 +0,0 @@ -// average-rating@2.0.4, by @ndaidong - built with esbuild at 2023-03-12T04:52:52.906Z - published under MIT license -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); -}; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); - -// src/main.js -var main_exports = {}; -__export(main_exports, { - average: () => average, - rate: () => rate, - score: () => score -}); -module.exports = __toCommonJS(main_exports); -var score = (p = 0, n = 0) => { - if (p === 0 && n === 0) { - return 0; - } - const r = ((p + 1.9208) / (p + n) - 1.96 * Math.sqrt(p * n / (p + n) + 0.9604) / (p + n)) / (1 + 3.8416 / (p + n)); - return Number(r.toFixed(2)); -}; -var rate = (rating = []) => { - const size = rating.length; - let n = rating[0]; - let p = rating[size - 1]; - const step = (1 / (size - 1)).toFixed(2); - const totalStep = size - 1; - for (let i = 1; i < totalStep; i++) { - const ep = (step * i).toFixed(2); - p += rating[i] * ep; - n += rating[totalStep - i] * ep; - } - return score(p, n); -}; -var average = (rating = []) => { - const total = rating.reduce((prev, current) => { - return prev + current; - }, 0); - if (total === 0) { - return 0; - } - let sum = 0; - let k = 1; - rating.forEach((item) => { - sum += item * k; - k++; - }); - const r = sum / total; - return Number(r.toFixed(1)); -}; -// Annotate the CommonJS export names for ESM import in node: -0 && (module.exports = { - average, - rate, - score -}); diff --git a/dist/cjs/package.json b/dist/cjs/package.json deleted file mode 100644 index 2660041..0000000 --- a/dist/cjs/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "average-rating", - "version": "2.0.4", - "main": "./average-rating.js" -} \ No newline at end of file diff --git a/eval.js b/eval.js deleted file mode 100644 index b185bc9..0000000 --- a/eval.js +++ /dev/null @@ -1,14 +0,0 @@ -// eval.js - -import { - score, - rate, - average -} from './src/main.js' - -const scoring = [80, 20] -const rating = [134055, 57472, 143135, 365957, 1448459] - -console.log(`score(${scoring.join(', ')}) // => ${score(...scoring)}`) -console.log(`average([${rating.join(', ')}]) // => ${average(rating)}`) -console.log(`rate([${rating.join(', ')}]) // => ${rate(rating)}`) diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..5c6c278 --- /dev/null +++ b/mod.ts @@ -0,0 +1,66 @@ +// mod.ts +// ref: https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval + +/** + * Calculate score for the systems of Positive/Negative rating + * + * @param p Positive rating value + * @param n Negative rating value + * @returns a float value from 0 to 1 + */ +export const score = (p: number = 0, n: number = 0): number => { + if (p === 0 && n === 0) { + return 0; + } + const r: number = ((p + 1.9208) / (p + n) - + 1.96 * Math.sqrt(p * n / (p + n) + 0.9604) / (p + n)) / + (1 + 3.8416 / (p + n)); + return Number(r.toFixed(2)); +}; + +/** + * Calculate score for the systems of 5 rating levels + * + * @param rating An array of rating value + * @returns a float value from 0 to 1 + */ +export const rate = (rating: number[] = []): number => { + const size: number = rating.length; + + let n: number = rating[0]; + let p: number = rating[size - 1]; + + const step: number = Number((1 / (size - 1)).toFixed(2)); + const totalStep: number = size - 1; + for (let i = 1; i < totalStep; i++) { + const ep: number = Number((step * i).toFixed(2)); + p += rating[i] * ep; + n += rating[totalStep - i] * ep; + } + return score(p, n); +}; + +/** + * Calculate average value for the systems of 5 rating levels + * + * @param rating An array of rating value + * @returns a float value from 0 to 1 + */ +export const average = (rating: number[] = []): number => { + const total: number = rating.reduce((prev, current) => { + return prev + current; + }, 0); + + if (total === 0) { + return 0; + } + + let sum: number = 0; + let k: number = 1; + rating.forEach((item) => { + sum += item * k; + k++; + }); + const r: number = sum / total; + return Number(r.toFixed(1)); +}; diff --git a/package.json b/package.json deleted file mode 100644 index 42e31d1..0000000 --- a/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "version": "2.0.4", - "name": "average-rating", - "description": "Calculate average score and rating based on Wilson Score Equation", - "homepage": "https://www.npmjs.com/package/average-rating", - "repository": { - "type": "git", - "url": "https://github.com/ndaidong/average-rating" - }, - "author": "@ndaidong", - "main": "./src/main.js", - "exports": { - "import": "./src/main.js", - "require": "./dist/cjs/average-rating.js" - }, - "type": "module", - "engines": { - "node": ">= 14" - }, - "scripts": { - "lint": "eslint .", - "lint:fix": "eslint --fix .", - "pretest": "npm run lint", - "test": "NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --verbose --coverage=true", - "build": "node build src/main.js", - "eval": "node eval", - "reset": "node reset" - }, - "devDependencies": { - "esbuild": "^0.17.11", - "eslint": "^8.36.0", - "jest": "^29.5.0" - }, - "keywords": [ - "rating", - "vote", - "poll", - "average", - "score", - "Wilson Score", - "util" - ], - "license": "MIT" -} diff --git a/reset.js b/reset.js deleted file mode 100644 index 8e31507..0000000 --- a/reset.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * reset.js - * @ndaidong -**/ - -import { - existsSync, - unlinkSync -} from 'fs' - -import { execSync } from 'child_process' - -const dirs = [ - 'dist', - 'docs', - '.nyc_output', - 'coverage', - 'node_modules', - '.nuxt', -] - -const files = [ - 'yarn.lock', - 'pnpm-lock.yaml', - 'package-lock.json', - 'coverage.lcov', -] - -dirs.forEach((d) => { - execSync(`rm -rf ${d}`) -}) - -files.forEach((f) => { - if (existsSync(f)) { - unlinkSync(f) - } -}) diff --git a/scripts/build_npm.ts b/scripts/build_npm.ts new file mode 100644 index 0000000..2e969c6 --- /dev/null +++ b/scripts/build_npm.ts @@ -0,0 +1,32 @@ +// scripts/build_npm.ts +import { build, emptyDir } from "@deno/dnt"; + +import pkg from "../deno.json" with { type: "json" }; + +const outputDir = "./npm"; + +await emptyDir(outputDir); + +await build({ + importMap: "deno.json", + entryPoints: ["./mod.ts"], + outDir: outputDir, + shims: { + deno: true, + }, + package: { + name: pkg.name, + version: pkg.version, + description: pkg.description, + author: pkg.author, + repository: pkg.repository, + bugs: { + url: `${pkg.homepage}/issues`, + }, + license: pkg.license, + }, + postBuild() { + Deno.copyFileSync("LICENSE", "npm/LICENSE"); + Deno.copyFileSync("README.md", "npm/README.md"); + }, +}); diff --git a/src/main.js b/src/main.js deleted file mode 100755 index f40effd..0000000 --- a/src/main.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Calculate average score and rating based on Wilson Score Equation - * Refer: https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval - **/ - -export const score = (p = 0, n = 0) => { - if (p === 0 && n === 0) { - return 0 - } - const r = ((p + 1.9208) / (p + n) - 1.96 * Math.sqrt(p * n / (p + n) + 0.9604) / (p + n)) / (1 + 3.8416 / (p + n)) - return Number(r.toFixed(2)) -} - -export const rate = (rating = []) => { - const size = rating.length - - let n = rating[0] - let p = rating[size - 1] - - const step = (1 / (size - 1)).toFixed(2) - const totalStep = size - 1 - for (let i = 1; i < totalStep; i++) { - const ep = (step * i).toFixed(2) - p += rating[i] * ep - n += rating[totalStep - i] * ep - } - return score(p, n) -} - -export const average = (rating = []) => { - const total = rating.reduce((prev, current) => { - return prev + current - }, 0) - - if (total === 0) { - return 0 - } - - let sum = 0 - let k = 1 - rating.forEach((item) => { - sum += item * k - k++ - }) - const r = sum / total - return Number(r.toFixed(1)) -} diff --git a/src/main.test.js b/tests/mod_test.ts similarity index 73% rename from src/main.test.js rename to tests/mod_test.ts index fa87bfa..4817865 100644 --- a/src/main.test.js +++ b/tests/mod_test.ts @@ -1,80 +1,89 @@ -// main.test +import { assertEquals } from "assert"; -/* eslint-env jest */ - -import { - score, - average, - rate -} from './main' +import { average, rate, score } from "../mod.ts"; const scoreSamples = [ { p: 0, n: 1, e: 0, - }, { + }, + { p: 0, n: 5, e: 0, - }, { + }, + { p: 0, n: 10, e: 0, - }, { + }, + { p: 0, n: 20, e: 0, - }, { + }, + { p: 0, n: 100, e: 0, - }, { + }, + { p: 0, n: 1000, e: 0, - }, { + }, + { n: 0, p: 1, e: 0.21, - }, { + }, + { n: 0, p: 5, e: 0.57, - }, { + }, + { n: 0, p: 10, e: 0.72, - }, { + }, + { n: 0, p: 20, e: 0.84, - }, { + }, + { n: 0, p: 100, e: 0.96, - }, { + }, + { n: 0, p: 1000, e: 1, - }, { + }, + { n: 1, p: 1, e: 0.09, - }, { + }, + { n: 5, p: 5, e: 0.24, - }, { + }, + { n: 500, p: 500, e: 0.47, - }, { + }, + { n: 1000, p: 1000, e: 0.48, }, -] +]; const ratingSamples = [ { @@ -83,104 +92,120 @@ const ratingSamples = [ average: 0, score: 0, }, - }, { + }, + { rating: [1, 1, 1, 1, 1], expect: { average: 3.0, score: 0.17, }, - }, { + }, + { rating: [2, 2, 2, 2, 2], expect: { average: 3.0, score: 0.24, }, - }, { + }, + { rating: [3, 3, 3, 3, 3], expect: { average: 3.0, score: 0.27, }, - }, { + }, + { rating: [4, 4, 4, 4, 4], expect: { average: 3.0, score: 0.3, }, - }, { + }, + { rating: [5, 5, 5, 5, 5], expect: { average: 3.0, score: 0.32, }, - }, { + }, + { rating: [5, 4, 3, 2, 1], expect: { average: 2.3, score: 0.15, }, - }, { + }, + { rating: [5, 0, 0, 0, 5], expect: { average: 3.0, score: 0.24, }, - }, { + }, + { rating: [5, 0, 0, 4, 5], expect: { average: 3.3, score: 0.33, }, - }, { + }, + { rating: [5, 4, 0, 0, 5], expect: { average: 2.7, score: 0.21, }, - }, { + }, + { rating: [0, 0, 0, 0, 5], expect: { average: 5, score: 0.57, }, - }, { + }, + { rating: [0, 0, 0, 4, 5], expect: { average: 4.6, score: 0.56, }, - }, { + }, + { rating: [0, 0, 3, 4, 5], expect: { average: 4.2, score: 0.51, }, - }, { + }, + { rating: [0, 2, 3, 4, 5], expect: { average: 3.9, score: 0.45, }, - }, { + }, + { rating: [1, 2, 3, 4, 5], expect: { average: 3.7, score: 0.42, }, - }, { + }, + { rating: [9524, 4158, 10177, 25971, 68669], expect: { average: 4.2, score: 0.79, }, - }, { + }, + { rating: [134055, 57472, 143135, 365957, 1448459], expect: { average: 4.4, score: 0.84, }, }, -] +]; const customRangeSamples = [ { @@ -203,32 +228,32 @@ const customRangeSamples = [ input: [125, 166, 17, 290, 400, 310, 1800], expect: 0.79, }, -] +]; -test('Testing `score()` method', () => { +Deno.test("check if score() works correctly", () => { scoreSamples.forEach((sample) => { - const actual = score(sample.p, sample.n) - expect(sample.e === actual).toBeTruthy() - }) -}) + const actual = score(sample.p, sample.n); + assertEquals(sample.e, actual); + }); +}); -test('Testing `average()` method', () => { +Deno.test("check if average() works correctly", () => { ratingSamples.forEach((sample) => { - const actual = average(sample.rating) - expect(sample.expect.average === actual).toBeTruthy() - }) -}) + const actual = average(sample.rating); + assertEquals(sample.expect.average, actual); + }); +}); -test('Testing `rate()` method', () => { +Deno.test("check if rate() works correctly", () => { ratingSamples.forEach((sample) => { - const actual = rate(sample.rating) - expect(sample.expect.score === actual).toBeTruthy() - }) -}) + const actual = rate(sample.rating); + assertEquals(sample.expect.score, actual); + }); +}); -test('Testing `rate()` method with custom range', () => { +Deno.test("check if rate() with custom range works correctly", () => { customRangeSamples.forEach((sample) => { - const actual = rate(sample.input) - expect(sample.expect === actual).toBeTruthy() - }) -}) + const actual = rate(sample.input); + assertEquals(sample.expect, actual); + }); +});