Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions providers/edge_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import edge, { type Edge } from 'edge.js'
import { type URLOptions } from '../types/http.ts'
import type { ApplicationService } from '../src/types.ts'
import numberHelpers from '../src/helpers/number.ts'
import { pluginEdgeDumper } from '../modules/dumper/plugins/edge.ts'
import { BriskRoute, HttpContext, Qs, type Route, type Router } from '../modules/http/main.ts'
import { type ClientRouteJSON } from '@adonisjs/http-server/client/url_builder'
Expand Down Expand Up @@ -145,6 +146,7 @@ export default class EdgeServiceProvider {

edge.global('app', app)
edge.global('config', edgeConfigResolver)
edge.global('number', numberHelpers)
edge.global('routes', function () {
return clientRoutes()
})
Expand Down
131 changes: 131 additions & 0 deletions src/helpers/number.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* @adonisjs/core
*
* (c) AdonisJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/**
* Collection of number helpers to clamp, parse, and format numeric values.
*
* @example
* // Constrain and inspect values
* numberHelpers.clamp(15, 0, 10) // 10
* numberHelpers.between(5, 0, 10) // true
*
* @example
* // Parse untrusted input and format output
* numberHelpers.parse('42') // 42
* numberHelpers.toFinite('abc', 0) // 0
* numberHelpers.format(1500, { compact: true }) // '1.5K'
*/
const numberHelpers = {
/**
* Constrain a number to stay within the given bounds.
*
* @param value - The number to constrain
* @param min - The lower bound
* @param max - The upper bound
*
* @example
* numberHelpers.clamp(15, 0, 10) // 10
* numberHelpers.clamp(-2, 0, 10) // 0
* numberHelpers.clamp(5, 0, 10) // 5
*/
clamp(value: number, min: number, max: number): number {
if (value < min) {
return min
} else if (value > max) {
return max
} else {
return value
}
},

/**
* Check if a number is inside an inclusive range. The bounds may be
* passed in either order.
*
* @param value - The number to test
* @param min - One end of the range
* @param max - The other end of the range
*
* @example
* numberHelpers.between(5, 0, 10) // true
* numberHelpers.between(0, 0, 10) // true
* numberHelpers.between(11, 0, 10) // false
* numberHelpers.between(5, 10, 0) // true
*/
between(value: number, min: number, max: number): boolean {
const lo = Math.min(min, max)
const hi = Math.max(min, max)
return value >= lo && value <= hi
},

/**
* Convert a value to a finite number. Returns the fallback when the
* result is `NaN` or `Infinity`.
*
* @param value - The value to convert
* @param fallback - Value to return when conversion fails. Defaults to `0`
*
* @example
* numberHelpers.toFinite(5) // 5
* numberHelpers.toFinite('42') // 42
* numberHelpers.toFinite(Number.NaN) // 0
* numberHelpers.toFinite(Number.POSITIVE_INFINITY) // 0
* numberHelpers.toFinite('abc', 10) // 10
*/
toFinite(value: unknown, fallback = 0): number {
const n = typeof value === 'number' ? value : Number(value)
return Number.isFinite(n) ? n : fallback
},

/**
* Parse a value into a finite number. Returns `null` for empty input,
* `NaN`, and `Infinity` instead of substituting a fallback.
*
* @param value - The value to parse
*
* @example
* numberHelpers.parse(5) // 5
* numberHelpers.parse('42') // 42
* numberHelpers.parse('') // null
* numberHelpers.parse(null) // null
* numberHelpers.parse(Number.NaN) // null
* numberHelpers.parse(Number.POSITIVE_INFINITY) // null
*/
parse(value: unknown): number | null {
if (value === null || value === undefined || value === '') {
return null
}

const n = typeof value === 'number' ? value : Number(value)
return Number.isFinite(n) ? n : null
},

/**
* Format a number using `Intl.NumberFormat`.
*
* @param value - The number to format
* @param options - Formatting options
* @param options.digits - Maximum fraction digits. Defaults to `2`
* @param options.compact - Use compact notation (e.g. `1.5K`)
*
* @example
* numberHelpers.format(12.3456) // '12.35'
* numberHelpers.format(12.3456, { digits: 1 }) // '12.3'
* numberHelpers.format(1500, { compact: true }) // '1.5K'
*/
format(value: number, options?: { digits?: number; compact?: boolean }): string {
return new Intl.NumberFormat('en', {
maximumFractionDigits: options?.digits ?? 2,
notation: options?.compact ? 'compact' : 'standard',
}).format(value)
},
}

export default numberHelpers

26 changes: 26 additions & 0 deletions tests/bindings/edge.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ test.group('Bindings | Edge', () => {
assert.isFalse(edge.globals.config.has('foobar'))
assert.strictEqual(edge.globals.app, app)
assert.instanceOf(edge.globals.qs, Qs)
assert.equal(edge.globals.number.clamp(15, 0, 10), 10)

const router = await app.container.make('router')
router.get('/users/:id', () => {})
Expand Down Expand Up @@ -81,6 +82,31 @@ test.group('Bindings | Edge', () => {
assert.equal(ctx.response.getBody(), 'Hello virk')
})

test('use number helpers inside templates', async ({ assert }) => {
const ignitor = new IgnitorFactory()
.merge({
rcFileContents: {
providers: [
() => import('../../providers/app_provider.js'),
() => import('../../providers/edge_provider.js'),
],
},
})
.withCoreConfig()
.create(BASE_URL)

const app = ignitor.createApp('console')
await app.init()
await app.boot()

edge.registerTemplate('score', {
template: `{{ number.clamp(score, 0, 10) }} {{ number.between(score, 0, 10) }} {{ number.toFinite(raw, 0) }} {{ number.parse(valid) }} {{ number.format(amount, { compact: true }) }}`,
})

const html = await edge.render('score', { score: 15, raw: 'abc', valid: '8', amount: 1500 })
assert.equal(html, '10 false 0 8 1.5K')
})

test('make form action using formAttributes helper', async ({ assert }) => {
const ignitor = new IgnitorFactory()
.merge({
Expand Down
45 changes: 45 additions & 0 deletions tests/helpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,51 @@
import { test } from '@japa/runner'
import StringBuilder from '@poppinss/utils/string_builder'
import stringHelpers from '../src/helpers/string.ts'
import numberHelpers from '../src/helpers/number.ts'

test.group('Number helpers', () => {
test('clamp value between min and max', ({ assert }) => {
assert.equal(numberHelpers.clamp(15, 0, 10), 10)
assert.equal(numberHelpers.clamp(-2, 0, 10), 0)
assert.equal(numberHelpers.clamp(5, 0, 10), 5)
})

test('check if number is between min and max', ({ assert }) => {
assert.isTrue(numberHelpers.between(5, 0, 10))
assert.isTrue(numberHelpers.between(0, 0, 10))
assert.isTrue(numberHelpers.between(10, 0, 10))
assert.isFalse(numberHelpers.between(-1, 0, 10))
assert.isFalse(numberHelpers.between(11, 0, 10))
assert.isTrue(numberHelpers.between(5, 10, 0))
})

test('convert value to a finite number', ({ assert }) => {
assert.equal(numberHelpers.toFinite(5), 5)
assert.equal(numberHelpers.toFinite('42'), 42)
assert.equal(numberHelpers.toFinite(Number.NaN), 0)
assert.equal(numberHelpers.toFinite(Number.POSITIVE_INFINITY), 0)
assert.equal(numberHelpers.toFinite(Number.NEGATIVE_INFINITY, 3), 3)
assert.equal(numberHelpers.toFinite('abc', 10), 10)
assert.equal(numberHelpers.toFinite(undefined), 0)
})

test('parse value into a finite number or null', ({ assert }) => {
assert.equal(numberHelpers.parse(5), 5)
assert.equal(numberHelpers.parse('42'), 42)
assert.isNull(numberHelpers.parse(''))
assert.isNull(numberHelpers.parse(null))
assert.isNull(numberHelpers.parse(undefined))
assert.isNull(numberHelpers.parse(Number.NaN))
assert.isNull(numberHelpers.parse(Number.POSITIVE_INFINITY))
assert.isNull(numberHelpers.parse('abc'))
})

test('format a number', ({ assert }) => {
assert.equal(numberHelpers.format(12.3456), '12.35')
assert.equal(numberHelpers.format(12.3456, { digits: 1 }), '12.3')
assert.equal(numberHelpers.format(1500, { compact: true }), '1.5K')
})
})

test.group('String helpers', () => {
test('check if string is empty', ({ assert }) => {
Expand Down