diff --git a/package.json b/package.json index d5231b8..de7bfe6 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,9 @@ "engines": { "node": ">=14" }, - "dependencies": {}, + "dependencies": { + "i18n-iso-countries": "^7.6.0" + }, "keywords": [ "rock climbing", "climbing grades" diff --git a/src/Disciplines.ts b/src/Disciplines.ts new file mode 100644 index 0000000..a524ea0 --- /dev/null +++ b/src/Disciplines.ts @@ -0,0 +1,63 @@ +/** + * What sort of climb is this? Routes can combine these fields, which is why + * this is not an enumeration. + * For example, a route may be a sport route, but also a top rope route. + */ +export interface DisciplineType { + /** https://en.wikipedia.org/wiki/Traditional_climbing */ + trad?: boolean + /** https://en.wikipedia.org/wiki/Sport_climbing */ + sport?: boolean + /** https://en.wikipedia.org/wiki/Bouldering */ + bouldering?: boolean + /** https://en.wikipedia.org/wiki/Deep-water_soloing */ + deepwatersolo?: boolean + /** https://en.wikipedia.org/wiki/Alpine_climbing */ + alpine?: boolean + /** https://en.wikipedia.org/wiki/Ice_climbing */ + snow?: boolean + /** https://en.wikipedia.org/wiki/Ice_climbing */ + ice?: boolean + /** https://en.wikipedia.org/wiki/Mixed_climbing */ + mixed?: boolean + /** https://en.wikipedia.org/wiki/Aid_climbing */ + aid?: boolean + /** https://en.wikipedia.org/wiki/Top_rope_climbing */ + tr?: boolean +} + +export const validDisciplines = [ + 'trad', + 'sport', + 'bouldering', + 'deepwatersolo', + 'alpine', + 'snow', + 'ice', + 'mixed', + 'aid', + 'tr' +] + +/** + * Perform runtime validation of climb discipline object + * @param disciplineObj IClimbType + */ +export const sanitizeDisciplines = (disciplineObj: Partial | undefined): DisciplineType | undefined => { + if (disciplineObj == null) return undefined + + const output = validDisciplines.reduce((acc, current) => { + if (disciplineObj?.[current] != null) { + acc[current] = disciplineObj[current] + } else { + acc[current] = false + } + return acc + }, {}) + // @ts-expect-error + if (disciplineObj?.boulder != null) { + // @ts-expect-error + output.bouldering = disciplineObj.boulder + } + return output as DisciplineType +} diff --git a/src/GradeContexts.ts b/src/GradeContexts.ts new file mode 100644 index 0000000..da5e46a --- /dev/null +++ b/src/GradeContexts.ts @@ -0,0 +1,224 @@ +import { getScale } from './GradeParser' +import { GradeScales, GradeScalesTypes } from './GradeScale' +import isoCountries from 'i18n-iso-countries' +// import { DisciplineType, ClimbGradeContextType } from './db/ClimbTypes.js' +import { DisciplineType } from './Disciplines' + +/** + * Grade systems have minor variations between countries. gradeContext is a + * short abbreviated string that identifies the context in which the grade was assigned + * and should signify a regional or national variation that may be considered within + * grade comparisons. + */ +export enum GradeContexts { + /** Alaska (United States) */ + ALSK = 'ALSK', + /** Australia */ + AU = 'AU', + BRZ = 'BRZ', + FIN = 'FIN', + FR = 'FR', + HK = 'HK', + NWG = 'NWG', + POL = 'POL', + SA = 'SA', + /** Sweden */ + SWE = 'SWE', + SX = 'SX', + UIAA = 'UIAA', + /** United Kingdom */ + UK = 'UK', + /** United States of Ameria */ + US = 'US' +} + +export type ClimbGradeContextType = Record + +/** + * A conversion from grade context to corresponding grade type / scale + * Todo: move this to @openbeta/sandbag + */ +export const gradeContextToGradeScales: Partial> = { + [GradeContexts.AU]: { + trad: GradeScales.EWBANK, + sport: GradeScales.EWBANK, + bouldering: GradeScales.VSCALE, + tr: GradeScales.EWBANK, + deepwatersolo: GradeScales.EWBANK, + alpine: GradeScales.YDS, + mixed: GradeScales.YDS, + aid: GradeScales.AID, + snow: GradeScales.YDS, // is this the same as alpine? + ice: GradeScales.WI + }, + [GradeContexts.US]: { + trad: GradeScales.YDS, + sport: GradeScales.YDS, + bouldering: GradeScales.VSCALE, + tr: GradeScales.YDS, + deepwatersolo: GradeScales.YDS, + alpine: GradeScales.YDS, + mixed: GradeScales.YDS, + aid: GradeScales.AID, + snow: GradeScales.YDS, // is this the same as alpine? + ice: GradeScales.WI + }, + [GradeContexts.FR]: { + trad: GradeScales.FRENCH, + sport: GradeScales.FRENCH, + bouldering: GradeScales.FONT, + tr: GradeScales.FRENCH, + deepwatersolo: GradeScales.FRENCH, + alpine: GradeScales.FRENCH, + mixed: GradeScales.FRENCH, + aid: GradeScales.AID, + snow: GradeScales.FRENCH, // is this the same as alpine? + ice: GradeScales.WI + }, + [GradeContexts.SA]: { + trad: GradeScales.FRENCH, + sport: GradeScales.FRENCH, + bouldering: GradeScales.FONT, + tr: GradeScales.FRENCH, + deepwatersolo: GradeScales.FRENCH, + alpine: GradeScales.FRENCH, + mixed: GradeScales.FRENCH, + aid: GradeScales.AID, + snow: GradeScales.FRENCH, // SA does not have a whole lot of snow + ice: GradeScales.WI + }, + [GradeContexts.UIAA]: { + trad: GradeScales.UIAA, + sport: GradeScales.UIAA, + bouldering: GradeScales.FONT, + tr: GradeScales.UIAA, + deepwatersolo: GradeScales.FRENCH, + alpine: GradeScales.UIAA, + mixed: GradeScales.UIAA, // TODO: change to MI scale, once added + aid: GradeScales.UIAA, + snow: GradeScales.UIAA, // TODO: remove `snow` since it duplicates `ice` + ice: GradeScales.WI + } +} + +/** + * Convert a human-readable grade to the appropriate grade object. + * @param gradeStr human-readable, eg: '5.9' or '5c'. + * @param disciplines the climb disciplines + * @param context grade context + * @returns grade object + */ +export const createGradeObject = (gradeStr: string, disciplines: DisciplineType | undefined, context: ClimbGradeContextType): Partial> | undefined => { + if (disciplines == null) return undefined + return Object.keys(disciplines).reduce> | undefined>((acc, curr) => { + if (disciplines[curr] === true) { + const scaleTxt = context[curr] + const scaleApi = getScale(scaleTxt) + if (scaleApi != null && !(scaleApi.getScore(gradeStr) < 0)) { + // only assign valid grade + if (acc == null) { + acc = { + [scaleTxt]: gradeStr + } + } else { + acc[scaleTxt] = gradeStr + } + } + } + return acc + }, undefined) +} + +/** + * A record of all countries with a default grade context that is not US + */ +const COUNTRIES_DEFAULT_NON_US_GRADE_CONTEXT: Record = { + AND: GradeContexts.FR, + ATF: GradeContexts.FR, + AUS: GradeContexts.AU, + AUT: GradeContexts.UIAA, + AZE: GradeContexts.UIAA, + BEL: GradeContexts.FR, + BGR: GradeContexts.UIAA, + BIH: GradeContexts.FR, + BLR: GradeContexts.UIAA, + BRA: GradeContexts.BRZ, + BWA: GradeContexts.SA, + CHE: GradeContexts.FR, + CUB: GradeContexts.FR, + CZE: GradeContexts.UIAA, + DEU: GradeContexts.UIAA, + DNK: GradeContexts.UIAA, + EGY: GradeContexts.FR, + ESP: GradeContexts.FR, + EST: GradeContexts.FR, + FIN: GradeContexts.FIN, + FRA: GradeContexts.FR, + GBR: GradeContexts.UK, + GRC: GradeContexts.FR, + GUF: GradeContexts.FR, + HKG: GradeContexts.HK, + HRV: GradeContexts.FR, + HUN: GradeContexts.UIAA, + IOT: GradeContexts.UK, + IRL: GradeContexts.UK, + ITA: GradeContexts.FR, + JEY: GradeContexts.UK, + JOR: GradeContexts.FR, + KEN: GradeContexts.UK, + KGZ: GradeContexts.FR, + LAO: GradeContexts.FR, + LIE: GradeContexts.FR, + LSO: GradeContexts.SA, + LTU: GradeContexts.FR, + LUX: GradeContexts.FR, + LVA: GradeContexts.FR, + MAR: GradeContexts.FR, + MCO: GradeContexts.FR, + MDA: GradeContexts.FR, + MDG: GradeContexts.FR, + MKD: GradeContexts.FR, + MLT: GradeContexts.FR, + MNE: GradeContexts.UIAA, + MYS: GradeContexts.FR, + NAM: GradeContexts.SA, + NCL: GradeContexts.FR, + NLD: GradeContexts.FR, + NOR: GradeContexts.NWG, + NZL: GradeContexts.AU, + PER: GradeContexts.FR, + PNG: GradeContexts.AU, + POL: GradeContexts.POL, + PRT: GradeContexts.FR, + PYF: GradeContexts.FR, + ROU: GradeContexts.FR, + RUS: GradeContexts.FR, + SGP: GradeContexts.FR, + SRB: GradeContexts.FR, + SVK: GradeContexts.UIAA, + SVN: GradeContexts.FR, + SWE: GradeContexts.SWE, + THA: GradeContexts.FR, + TON: GradeContexts.AU, + TUN: GradeContexts.FR, + TUR: GradeContexts.FR, + UGA: GradeContexts.SA, + UKR: GradeContexts.FR, + VNM: GradeContexts.FR, + ZAF: GradeContexts.SA +} + +/** + * + * @returns all countries with their default grade context + */ +export const getCountriesDefaultGradeContext = (): { [x: string]: GradeContexts } => { + const countries = { ...COUNTRIES_DEFAULT_NON_US_GRADE_CONTEXT } + for (const alpha3Code in isoCountries.getAlpha3Codes()) { + // Any country not found will have a US Grade Context + if (!(alpha3Code in countries)) { + countries[alpha3Code] = GradeContexts.US + } + } + return countries +} diff --git a/src/index.ts b/src/index.ts index 83b25c8..1a01cc8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,3 @@ - import { GradeScales, GradeScalesTypes } from './GradeScale' import { getScale, @@ -10,6 +9,8 @@ import { import { GradeBands, GradeBandTypes } from './GradeBands' import { AI, Aid, Ewbank, Font, French, Norwegian, Saxon, UIAA, VScale, WI, YosemiteDecimal } from './scales' +import { GradeContexts, gradeContextToGradeScales, getCountriesDefaultGradeContext } from './GradeContexts' + // Free Climbing Grades // YDS // French @@ -313,3 +314,5 @@ export { WI, YosemiteDecimal } + +export { GradeContexts, gradeContextToGradeScales, getCountriesDefaultGradeContext } diff --git a/yarn.lock b/yarn.lock index a9b5773..1ca8ca4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3263,6 +3263,11 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +diacritics@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1" + integrity sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA== + diff-sequences@^25.2.6: version "25.2.6" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" @@ -4638,6 +4643,13 @@ husky@^7.0.4: resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== +i18n-iso-countries@^7.6.0: + version "7.6.0" + resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-7.6.0.tgz#4e2eac7043210a5552e7fd116b74d4f36a90b960" + integrity sha512-HPKjOUKS0BkjiY4ayrsuFbu7Ock++pXLs+FAOYl4WfTL5L0ploEH68fiRAP6Zev5g/jvMFt54KcPGJcb942wbg== + dependencies: + diacritics "1.3.0" + iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -8374,7 +8386,7 @@ typescript@^3.7.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== -typescript@^4.5.5: +typescript@^4.9.5: version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==