Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[HackerNews] Show User Karma #7411

Merged
merged 11 commits into from
Dec 28, 2021
64 changes: 64 additions & 0 deletions services/hackernews/hackernews-user-karma.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Joi from 'joi'
import { metric } from '../text-formatters.js'
import { BaseJsonService, NotFound } from '../index.js'
import { anyInteger } from '../validators.js'

const schema = Joi.object({
karma: anyInteger,
})
.allow(null)
.required()

export default class HackerNewsUserKarma extends BaseJsonService {
static category = 'social'

static route = {
base: 'hackernews/user-karma',
pattern: ':id',
}

static examples = [
{
title: 'HackerNews User Karma',
namedParams: { id: 'pg' },
staticPreview: this.render({ id: 'pg', karma: 15536 }),
},
]

static defaultBadgeData = {
label: 'Karma',
namedLogo: 'ycombinator',
}

static render({ karma, id }) {
const color = karma > 0 ? 'brightgreen' : karma === 0 ? 'orange' : 'red'
return {
label: id,
message: metric(karma),
chris48s marked this conversation as resolved.
Show resolved Hide resolved
color,
style: 'social',
}
}

async fetch({ id }) {
return this._requestJson({
schema,
url: `https://hacker-news.firebaseio.com/v0/user/${id}.json`,
errorMessages: {
404: 'user not found',
},
})
}

async handle({ id }) {
const json = await this.fetch({ id })
if (json == null) {
throw new NotFound({ prettyMessage: 'user not found' })
}
const { karma } = json
return this.constructor.render({
karma,
id,
})
}
}
26 changes: 26 additions & 0 deletions services/hackernews/hackernews-user-karma.tester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createServiceTester } from '../tester.js'
import { isMetricAllowNegative } from '../test-validators.js'

export const t = await createServiceTester()

t.create('valid repo').get('/pg.json').expectBadge({
label: 'pg',
message: isMetricAllowNegative,
})

t.create('valid repo -- negative karma')
.get('/negative.json')
.intercept(nock =>
nock('https://hacker-news.firebaseio.com/v0/user')
.get('/negative.json')
.reply(200, { karma: -1234 })
)
.expectBadge({
label: 'negative',
message: isMetricAllowNegative,
})

t.create('invalid user').get('/hopefullythisdoesnotexist.json').expectBadge({
label: 'Karma',
message: 'user not found',
})
6 changes: 6 additions & 0 deletions services/test-validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ const isStarRating = withRegex(
// Required to be > 0, because accepting zero masks many problems.
const isMetric = withRegex(/^([1-9][0-9]*[kMGTPEZY]?|[1-9]\.[1-9][kMGTPEZY])$/)

// Same as isMetric, but tests for negative numbers also.
const isMetricAllowNegative = withRegex(
/^(0|-?[1-9][0-9]*[kMGTPEZY]?|-?[0-9]\.[0-9][kMGTPEZY])$/
)

/**
* @param {RegExp} nestedRegexp Pattern that must appear after the metric.
* @returns {Function} A function that returns a RegExp that matches a metric followed by another pattern.
Expand Down Expand Up @@ -159,6 +164,7 @@ export {
isPhpVersionReduction,
isStarRating,
isMetric,
isMetricAllowNegative,
isMetricWithPattern,
isMetricOpenIssues,
isMetricOverMetric,
Expand Down
16 changes: 10 additions & 6 deletions services/text-formatters.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,27 +46,31 @@ function ordinalNumber(n) {
return n + (s[(v - 20) % 10] || s[v] || s[0])
}

// Given a number, string with appropriate unit in the metric system, SI.
// Given a number (positive or negative), string with appropriate unit in the metric system, SI.
chris48s marked this conversation as resolved.
Show resolved Hide resolved
// Note: numbers beyond the peta- cannot be represented as integers in JS.
const metricPrefix = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
const metricPower = metricPrefix.map((a, i) => Math.pow(1000, i + 1))
function metric(n) {
for (let i = metricPrefix.length - 1; i >= 0; i--) {
const limit = metricPower[i]
if (n >= limit) {
const scaledN = n / limit
const absN = Math.abs(n)
if (absN >= limit) {
const scaledN = absN / limit
if (scaledN < 10) {
// For "small" numbers, display one decimal digit unless it is 0.
const oneDecimalN = scaledN.toFixed(1)
if (oneDecimalN.charAt(oneDecimalN.length - 1) !== '0') {
return `${oneDecimalN}${metricPrefix[i]}`
const res = `${oneDecimalN}${metricPrefix[i]}`
return n > 0 ? res : `-${res}`
}
}
const roundedN = Math.round(scaledN)
if (roundedN < 1000) {
return `${roundedN}${metricPrefix[i]}`
const res = `${roundedN}${metricPrefix[i]}`
return n > 0 ? res : `-${res}`
} else {
return `1${metricPrefix[i + 1]}`
const res = `1${metricPrefix[i + 1]}`
return n > 0 ? res : `-${res}`
}
}
}
Expand Down
22 changes: 22 additions & 0 deletions services/text-formatters.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe('Text formatters', function () {
test(metric, () => {
/* eslint-disable no-loss-of-precision */
/* eslint-disable @typescript-eslint/no-loss-of-precision */
given(0).expect('0')
given(999).expect('999')
given(1000).expect('1k')
given(1100).expect('1.1k')
Expand All @@ -60,6 +61,27 @@ describe('Text formatters', function () {
given(1100000000000000000000).expect('1.1Z')
given(2222222222222222222222222).expect('2.2Y')
given(22222222222222222222222222).expect('22Y')
given(-999).expect('-999')
Comment on lines 63 to +64
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets add a test for zero in here too

Suggested change
given(22222222222222222222222222).expect('22Y')
given(-999).expect('-999')
given(22222222222222222222222222).expect('22Y')
given(0).expect('0')
given(-999).expect('-999')

given(-999).expect('-999')
given(-1000).expect('-1k')
given(-1100).expect('-1.1k')
given(-10100).expect('-10k')
given(-999499).expect('-999k')
given(-999500).expect('-1M')
given(-1100000).expect('-1.1M')
given(-1578896212).expect('-1.6G')
given(-20000000000).expect('-20G')
given(-15788962120).expect('-16G')
given(-9949999999999).expect('-9.9T')
given(-9950000000001).expect('-10T')
given(-4000000000000001).expect('-4P')
given(-4200000000000001).expect('-4.2P')
given(-7100700010058000200).expect('-7.1E')
given(-71007000100580002000).expect('-71E')
given(-1000000000000000000000).expect('-1Z')
given(-1100000000000000000000).expect('-1.1Z')
given(-2222222222222222222222222).expect('-2.2Y')
given(-22222222222222222222222222).expect('-22Y')
/* eslint-enable */
})

Expand Down