From ee686a3207d2e09af873bd2cca3664c974a27da0 Mon Sep 17 00:00:00 2001 From: ulihhhh <54112476+ulihhhh@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:17:13 +0200 Subject: [PATCH] fix(gbfs): read underscores in station names as word breaks JCDecaux's Spanish contracts prefix every station name with "_", and Valenbisi joins its words with it ("_PLAZA_MUSICO_LOPEZ_CHAVARRI"), so the card and HUD would show the raw identifier. The shared parser now reads a run of underscores, with the spaces around it, as one word break and trims the ends. It applies to `name`, the `short_name` fallback and GBFS v3 localized names, so the layer and get_bike_share both read it. No registered system changes: run over all 52 live station_information feeds on 2026-10-17, the old and new parsers give identical names for all 13,397 stations. Other spacing stays as the operator published it. Co-Authored-By: Claude Opus 5.5 --- src/sources/gbfsStations.js | 14 ++++++++++-- src/sources/gbfsStations.test.mjs | 36 +++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/sources/gbfsStations.js b/src/sources/gbfsStations.js index 74f2efd23..dd4eff898 100644 --- a/src/sources/gbfsStations.js +++ b/src/sources/gbfsStations.js @@ -51,15 +51,25 @@ export function extractStationsArray(payload) { return []; } +/** + * Station name text as a reader expects it. JCDecaux's Spanish contracts + * (Valenbisi, Sevici) prefix every name with "_" and Valenbisi joins words + * with it ("_PLAZA_MUSICO_LOPEZ_CHAVARRI"), so an underscore reads as a word + * break. Other spacing is the operator's own and stays as published. + */ +function tidyStationName(text) { + return text.replace(/\s*_+\s*/g, ' ').trim(); +} + /** Read legacy text or a GBFS v3 Localized String, preferring usable English text. */ function stationName(value) { - if (typeof value === 'string') return value.trim(); + if (typeof value === 'string') return tidyStationName(value); if (!Array.isArray(value)) return ''; let fallback = ''; for (const translation of value) { if (typeof translation?.text !== 'string') continue; - const text = translation.text.trim(); + const text = tidyStationName(translation.text); if (!text) continue; if (!fallback) fallback = text; if ( diff --git a/src/sources/gbfsStations.test.mjs b/src/sources/gbfsStations.test.mjs index 8c60da90c..6f12db258 100644 --- a/src/sources/gbfsStations.test.mjs +++ b/src/sources/gbfsStations.test.mjs @@ -22,6 +22,42 @@ test('GBFS v2 station names remain trimmed strings', () => { ); }); +test('underscores in station names read as word breaks', () => { + // As published by JCDecaux's Spanish contracts (checked 2026-10-17). + for (const [published, shown] of [ + ['_PLAZA_MUSICO_LOPEZ_CHAVARRI', 'PLAZA MUSICO LOPEZ CHAVARRI'], + ['_GLORIETA OLIMPICA', 'GLORIETA OLIMPICA'], + ['_CAMPAMENTO_81', 'CAMPAMENTO 81'], + ['Plaza__Mayor_ ', 'Plaza Mayor'], + ]) { + assert.equal(parseName({ name: published }), shown); + } +}); + +test('spacing without underscores stays as the operator published it', () => { + assert.equal(parseName({ name: 'C/ NÀPOLS, 82' }), 'C/ NÀPOLS, 82'); +}); + +test('a name of only underscores falls back to short_name, then to empty', () => { + assert.equal(parseName({ name: '_', short_name: '_S_1_' }), 'S 1'); + assert.equal(parseName({ name: '__', short_name: '_' }), ''); +}); + +test('underscores are read the same in GBFS v3 localized names', () => { + assert.equal( + parseName( + { + name: [ + { language: 'es', text: '_' }, + { language: 'en', text: '_COLON_I' }, + ], + }, + '3.0', + ), + 'COLON I', + ); +}); + test('GBFS v3 station names prefer English over the first translation', () => { assert.equal( parseName(