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

feat: US aid grades #96

Merged
merged 2 commits into from
Jun 19, 2023
Merged
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

Javascript utilities for working with rock climbing grades.

### Supported systems
### Supported systems

**Sport & Traditional climbing**
- [x] Yosemite Decimal System
Expand All @@ -19,6 +19,10 @@ Javascript utilities for working with rock climbing grades.
- [x] Vermin (V-scale)
- [x] Fontainebleau

**Aid**
- [x] A# & C#
- [ ] Aid with mandatory free climbing (5.8 A0, etc)

**Ice**
- [x] Winter Ice (WI#)
- [x] Alpine Ice (AI#)
Expand Down
1 change: 1 addition & 0 deletions src/GradeScale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default interface GradeScale {

export const GradeScales = {
AI: 'ai',
AID: 'aid',
WI: 'wi',
VSCALE: 'vscale',
YDS: 'yds',
Expand Down
37 changes: 37 additions & 0 deletions src/data/aid.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
Score,Aid
0,A0
1,A0
2,A0
3,A0
4,A1
5,A1
6,A1
7,A1
8,A2
9,A2
10,A2
11,A2
12,A2+
13,A2+
14,A2+
15,A2+
16,A3
17,A3
18,A3
19,A3
20,A3+
21,A3+
22,A3+
23,A3+
24,A4
25,A4
26,A4
27,A4
28,A4+
29,A4+
30,A4+
31,A4+
32,A5
33,A5
34,A5
35,A5
1 change: 1 addition & 0 deletions src/data/aid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"score":0,"aid":"A0"},{"score":1,"aid":"A0"},{"score":2,"aid":"A0"},{"score":3,"aid":"A0"},{"score":4,"aid":"A1"},{"score":5,"aid":"A1"},{"score":6,"aid":"A1"},{"score":7,"aid":"A1"},{"score":8,"aid":"A2"},{"score":9,"aid":"A2"},{"score":10,"aid":"A2"},{"score":11,"aid":"A2"},{"score":12,"aid":"A2+"},{"score":13,"aid":"A2+"},{"score":14,"aid":"A2+"},{"score":15,"aid":"A2+"},{"score":16,"aid":"A3"},{"score":17,"aid":"A3"},{"score":18,"aid":"A3"},{"score":19,"aid":"A3"},{"score":20,"aid":"A3+"},{"score":21,"aid":"A3+"},{"score":22,"aid":"A3+"},{"score":23,"aid":"A3+"},{"score":24,"aid":"A4"},{"score":25,"aid":"A4"},{"score":26,"aid":"A4"},{"score":27,"aid":"A4"},{"score":28,"aid":"A4+"},{"score":29,"aid":"A4+"},{"score":30,"aid":"A4+"},{"score":31,"aid":"A4+"},{"score":32,"aid":"A5"},{"score":33,"aid":"A5"},{"score":34,"aid":"A5"},{"score":35,"aid":"A5"}]
19 changes: 18 additions & 1 deletion src/data/csvtojson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import csv from 'csv-parser'
import * as fs from 'fs'
import { Boulder, Route, IceGrade } from '../scales'
import { AidGrade, Boulder, Route, IceGrade } from '../scales'
import path from 'path'

const boulderGrades: Boulder[] = []
Expand Down Expand Up @@ -70,3 +70,20 @@ fs.createReadStream(path.join(process.cwd(), 'src/data/ice.csv'))
const data = JSON.stringify(iceGrades)
fs.writeFileSync(`${writeDir}/ice.json`, data)
})

const aidGrades: AidGrade[] = []
fs.createReadStream(path.join(process.cwd(), 'src/data/aid.csv'))
.pipe(csv())
.on('data', (data) => {
if (data.Aid === '' && data.Aid === '') {
return
}
aidGrades.push({
score: parseInt(data.Score, 10),
aid: data.Aid
})
})
.on('end', () => {
const data = JSON.stringify(aidGrades)
fs.writeFileSync(`${writeDir}/aid.json`, data)
})
19 changes: 13 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
convertGrade
} from './GradeParser'
import { GradeBands, GradeBandTypes } from './GradeBands'
import { YosemiteDecimal, French, UIAA, Font, VScale, Ewbank, Norwegian } from './scales'
import { AI, Aid, Ewbank, Font, French, Norwegian, Saxon, UIAA, VScale, WI, YosemiteDecimal } from './scales'

// Free Climbing Grades
// YDS
Expand Down Expand Up @@ -297,12 +297,19 @@ export {
GradeScales,
GradeScalesTypes,
GradeBands,
GradeBandTypes,
YosemiteDecimal,
GradeBandTypes
}

export {
AI,
Aid,
Ewbank,
Font,
French,
Norwegian,
Saxon,
UIAA,
Font,
VScale,
Ewbank,
Norwegian
WI,
YosemiteDecimal
}
2 changes: 1 addition & 1 deletion src/scales/__tests__/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('AI', () => {
expect(score).toEqual(-1)
})

test('not aid scale', () => {
test('not AI scale', () => {
const score = AI.getScore('v11')
expect(console.warn).toHaveBeenCalledWith('Unexpected grade format: v11 for grade scale AI')
expect(score).toEqual(-1)
Expand Down
127 changes: 127 additions & 0 deletions src/scales/__tests__/aid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { Aid } from '../../scales'

describe('Aid', () => {
describe('Get Score', () => {
describe('valid grade formats', () => {
jest.spyOn(console, 'warn').mockImplementation()
beforeEach(() => {
jest.clearAllMocks()
})

test('basic grade A', () => {
const score = Aid.getScore('A0')
expect(console.warn).not.toHaveBeenCalled()
expect(score).not.toEqual(-1)
})

test('basic grade C', () => {
const score = Aid.getScore('C0')
expect(console.warn).not.toHaveBeenCalled()
expect(score).not.toEqual(-1)
})

test('valid + modifier', () => {
const score = Aid.getScore('A3+')
expect(console.warn).not.toHaveBeenCalled()
expect(score).not.toEqual(-1)
})

test.failing('mandatory free', () => {
const score = Aid.getScore('5.9 A0')
expect(console.warn).not.toHaveBeenCalled()
expect(score).not.toEqual(-1)
})
})

describe('invalid grade formats', () => {
jest.spyOn(console, 'warn').mockImplementation()
beforeEach(() => {
jest.clearAllMocks()
})

test('A6 out of range', () => {
const score = Aid.getScore('A6')
expect(console.warn).toHaveBeenCalledWith('Unexpected grade format: A6 for grade scale Aid')
expect(score).toEqual(-1)
})

test('invalid minus modifier', () => {
const score = Aid.getScore('A3-')
expect(console.warn).toHaveBeenCalledWith('Unexpected grade format: A3- for grade scale Aid')
expect(score).toEqual(-1)
})

test('invalid plus modifier', () => {
const score = Aid.getScore('A5+')
expect(console.warn).toHaveBeenCalledWith('Unexpected grade format: A5+ for grade scale Aid')
expect(score).toEqual(-1)
})

test('plain YDS grade', () => {
const score = Aid.getScore('5.9')
expect(console.warn).toHaveBeenCalledWith('Unexpected grade format: 5.9 for grade scale Aid')
expect(score).toEqual(-1)
})

test('slash grade', () => {
const score = Aid.getScore('A0/A1')
expect(console.warn).toHaveBeenCalledWith('Unexpected grade format: A0/A1 for grade scale Aid')
expect(score).toEqual(-1)
})

test('not aid scale', () => {
const score = Aid.getScore('v11')
expect(console.warn).toHaveBeenCalledWith('Unexpected grade format: v11 for grade scale Aid')
expect(score).toEqual(-1)
})
})

describe('correct relative scores', () => {
test('C0 = A0', () => {
const aGrade = Aid.getScore('A0')
const cGrade = Aid.getScore('C0')
expect(cGrade).toEqual(aGrade)
})

test('A3+ > A1', () => {
const lowGrade = Aid.getScore('A1')
const highGrade = Aid.getScore('A3+')
expect(highGrade[0]).toBeGreaterThan(lowGrade[1])
})

test('A4 > A3+', () => {
const lowGrade = Aid.getScore('A3+')
const highGrade = Aid.getScore('A4')
expect(highGrade[0]).toBeGreaterThan(lowGrade[1])
})

test('C4 > C3+', () => {
const lowGrade = Aid.getScore('A3+')
const highGrade = Aid.getScore('A4')
expect(highGrade[0]).toBeGreaterThan(lowGrade[1])
})

test('C4 > A3+', () => {
const lowGrade = Aid.getScore('A3+')
const highGrade = Aid.getScore('A4')
expect(highGrade[0]).toBeGreaterThan(lowGrade[1])
})

test('C5 = A5', () => {
const aGrade = Aid.getScore('A5')
const cGrade = Aid.getScore('C5')
expect(cGrade).toEqual(aGrade)
})
})
})

describe('Get Grade', () => {
test('bottom of range', () => {
expect(Aid.getGrade(0)).toBe('A0')
})

test('top of range', () => {
expect(Aid.getGrade(Infinity)).toBe('A5')
})
})
})
2 changes: 1 addition & 1 deletion src/scales/__tests__/wi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('WI', () => {
expect(score).toEqual(-1)
})

test('not aid scale', () => {
test('not WI scale', () => {
const score = WI.getScore('v11')
expect(console.warn).toHaveBeenCalledWith('Unexpected grade format: v11 for grade scale WI')
expect(score).toEqual(-1)
Expand Down
61 changes: 61 additions & 0 deletions src/scales/aid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import GradeScale, { findScoreRange, getAvgScore, GradeScales, Tuple } from '../GradeScale'
import aid_table from '../data/aid.json'
import { AidGrade } from '.'
import { GradeBandTypes, routeScoreToBand } from '../GradeBands'

// Supports [AC]0 -> [AC]5, with + grades on [AC]2 -> [AC]4 and no slash grades
// https://en.wikipedia.org/wiki/Grade_(climbing)#Clean_scale
const aidGradeRegex = /^([AC])([0-5]|[2-4]\+)$/i
const isAid = (grade: string): RegExpMatchArray | null => grade.match(aidGradeRegex)

const AidScale: GradeScale = {
displayName: 'Aid Grade',
name: GradeScales.AID,
offset: 1000,
allowableConversionType: [],
isType: (grade: string): boolean => {
if (isAid(grade) === null) {
return false
}
return true
},
getScore: (grade: string): number | Tuple => {
return getScore(grade)
},
getGrade: (score: number | Tuple): string => {
const validateScore = (score: number): number => {
const validScore = Number.isInteger(score) ? score : Math.ceil(score)
return Math.min(Math.max(0, validScore), aid_table.length - 1)
}

if (typeof score === 'number') {
return aid_table[validateScore(score)].aid
}

const low: string = aid_table[validateScore(score[0])].aid
const high: string = aid_table[validateScore(score[1])].aid
if (low === high) return low
return `${low}/${high}`
},
getGradeBand: (grade: string): GradeBandTypes => {
const score = getScore(grade)
return routeScoreToBand(getAvgScore(score))
}
}

const getScore = (grade: string): number | Tuple => {
const parse = isAid(grade)
if (parse == null) {
console.warn(`Unexpected grade format: ${grade} for grade scale Aid`)
return -1
}
const [wholeMatch, AorC, gradeNum] = parse // eslint-disable-line @typescript-eslint/no-unused-vars

const score = findScoreRange((r: AidGrade) => {
return r.aid === ('A' + gradeNum)
}, aid_table)

return score
}

export default AidScale
11 changes: 9 additions & 2 deletions src/scales/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import Ewbank from './ewbank'
import Saxon from './saxon'
import Norwegian from './norwegian'
import AI from './ai'
import Aid from './aid'
import WI from './wi'
import UIAA from './uiaa'
import GradeScale, { GradeScales } from '../GradeScale'
export { VScale, Font, YosemiteDecimal, French, Saxon, UIAA, Ewbank, AI, WI, Norwegian }
export { Aid, VScale, Font, YosemiteDecimal, French, Saxon, UIAA, Ewbank, AI, WI, Norwegian }

export interface Boulder {
score: number
Expand All @@ -36,6 +37,11 @@ export interface IceGrade {
wi: string
}

export interface AidGrade {
score: number
aid: string
}

export const scales: Record<
typeof GradeScales[keyof typeof GradeScales],
GradeScale | null
Expand All @@ -49,5 +55,6 @@ GradeScale | null
[GradeScales.SAXON]: Saxon,
[GradeScales.NORWEGIAN]: Norwegian,
[GradeScales.AI]: AI,
[GradeScales.WI]: WI
[GradeScales.WI]: WI,
[GradeScales.AID]: Aid
}
Loading