diff --git a/builders/JPN-JPN.js b/builders/JPN-JPN.js new file mode 100644 index 0000000..d710ede --- /dev/null +++ b/builders/JPN-JPN.js @@ -0,0 +1,83 @@ +const _ = require('lodash'); + +// get the value of a scalar, or the first element of an array if an array is passed +function scalarOrFirstElement(param) { + return _.isArray(param) ? param[0] : param; +} + +function formatPostalCode(record) { + if (record.postalcode) { + return '〒' + record.postalcode; + } +} + +function buildAdminLabelPart(schema, record) { + let labelParts = []; + + // iterate the admin components in the schema + // the order of the properties in the file determine + // the order of elements in this array + for (const field in schema.valueFunctions) { + const valueFunction = schema.valueFunctions[field]; + + labelParts.push(valueFunction(record)); + } + + return labelParts; +} + +// generally, our data from OA contains the 'chome' or district (similar to a neighbourhood) +// in the street field, even though it really is not a street at all +// detect this case and then return the street value +function formatDistrictPart(record) { + if (scalarOrFirstElement(record.name.default).includes(record.street)) { + return record.street; + } +} + +// check for records with a standard Japanese building and block number in the housenumber field +// these consist of two sets of numbers separated by a - +// for Japanese format language, reformat them with the "番号" or "bango" characters +function formatBlockPart(record) { + const bangoRegex = new RegExp(/(\d+)-(\d+)/); + + const match = bangoRegex.exec(record.housenumber); + if (match) { + return `${match[1]}番${match[2]}号`; + } + + return record.housenumber; +} + +function venueName(record) { + if (record.layer !== 'venue') { + return; + } + + return scalarOrFirstElement(record.name.ja) || scalarOrFirstElement(record.name.default); +} + +// builds a complete label by combining several components +// first, the postalcode if it exists, prefixed by the postal mark +// second, administrative information like city +// third, street/block level info if it exists, formatted as correctly as possible +// finally, the venue name if it exists +function japanBuilder(schema, record) { + + const postalcode = formatPostalCode(record); + const admin = buildAdminLabelPart(schema, record); + const distric = formatDistrictPart(record); + const block = formatBlockPart(record); + const venue = venueName(record); + + // combine components, inserting a space between the postalcode, admin, and name sections + // when written on an envelope these would correspond to new lines, a space should help with some clarity + let labelParts = _.concat(postalcode, ' ', admin, distric, block, ' ', venue); + + // retain only things that are truthy + labelParts = _.compact(labelParts); + + return labelParts; +} + +module.exports = japanBuilder; diff --git a/labelSchema.js b/labelSchema.js index f3062d7..f01a357 100644 --- a/labelSchema.js +++ b/labelSchema.js @@ -253,6 +253,28 @@ module.exports = { } }, 'JPN': { + 'languages': { + 'JPN': { + 'valueFunctions': { + 'prefecture': getFirstProperty(['region']), + + // outside of designated cities, this is the city name + 'city': getFirstProperty(['county']), + + // 20 important cities in Japan are known as 'designated cities' + // https://en.wikipedia.org/wiki/Cities_designated_by_government_ordinance_of_Japan + 'designated-city': getFirstProperty(['locality']), + + 'machi': getFirstProperty(['borough']), // this is a middle layer between city and district for larger cities + + 'district': getFirstProperty(['neighbourhood']), + }, + 'meta': { + 'builder': require('./builders/JPN-JPN'), + 'separator': '' // no separation between most elements in Japanese + } + } + }, 'valueFunctions': { 'district': getFirstProperty(['neighbourhood']), 'machi': getFirstProperty(['borough']), // this is a middle layer between city and district for larger cities diff --git a/test/getSchema.js b/test/getSchema.js index 0b5b3b0..c09026c 100644 --- a/test/getSchema.js +++ b/test/getSchema.js @@ -68,6 +68,14 @@ module.exports.tests.schema_and_language_selection = function(test, common) { t.equals(actual, expected, 'USA schema selected'); t.end(); }); + + test('valid language and valid country_a combination selects that language variant for country', function(t) { + const actual = getSchema({ country_a: ['JPN']}, 'JPN'); // There is a Japanese specific label format for records in Japan + const expected = schemas.JPN.languages.JPN; + + t.equals(actual, expected, 'Japanese schema for Japanese language selected'); + t.end(); + }); }; module.exports.all = function (tape, common) { diff --git a/test/labelGenerator_JPN_JPN.js b/test/labelGenerator_JPN_JPN.js new file mode 100644 index 0000000..da5db55 --- /dev/null +++ b/test/labelGenerator_JPN_JPN.js @@ -0,0 +1,164 @@ +const generator = require('../labelGenerator'); + +module.exports.tests = {}; + +module.exports.tests.english_style_labels = function(test, common) { + test('tokyo post office', function(t) { + const doc = { + name: { default: '東京中央郵便局' }, + layer: 'venue', + housenumber: '2', // this is incorrect but it's what we currently pull from OSM + locality: ['東京'], + county: ['千代田'], + region: ['東京'], + region_a: ['TK'], // should not be used + postalcode: ['100-0005'], + country_a: ['JPN'], // should not be used + country: ['日本'], // should not be used (though this is up for debate) + continent: ['アジア'], // should not be used + }; + + const expected = '〒100-0005 ' + // postalcode with postal mark, plus a space (would be a newline on an envelope) + '東京' + // region, tokyo-to + '千代田' + // county, chyoda ward + '東京' + // locality, tokyo + '2 ' + // housenumber plus space to separate venue name + '東京中央郵便局'; // venue name: 'Tokyo Central Post Office' + t.equal(generator(doc, 'JPN'), expected); + t.end(); + }); + + test('tokyo address from OA', function(t) { + const doc = { + name: { default: '7-9 丸の内二丁目' }, + layer: 'address', + housenumber: '7-9', + street: '丸の内二丁目', // Marunouchi 2-chome + borough: ['千代田区'], // Chiyoda Ward + locality: ['東京'], // Tokyo + county: ['千代田'], // Chiyoda + region: ['東京'], // Tokyo (should really be 東京都 for Tokyo-to) + region_a: ['TK'], + country: ['日本'], // Japan + country_a: ['JPN'], + continent: ['アジア'], // Asia + }; + + const expected = '' + // no postalcode for this record + '東京' + // prefecture (region), tokyo + '千代田' + // county, chyoda + '東京' + // designated city (locality), tokyo + '千代田区' + // ward (borough), chiyoda ward + '丸の内二丁目' + // district (street), Marunouchi 2-chome + '7番9号'; // "bango" (block and building number) + t.equal(generator(doc, 'JPN'), expected); + t.end(); + }); + + test('Shizuoka (smaller city near Tokyo) example from OA', function(t) { + const doc = { + name: { default: '331-8 中原' }, + layer: 'address', + housenumber: '331-8', + street: '中原', + // we should ideally have lower level admin info here, but we don't as of this writing + locality: ['静岡市'], + county: ['静岡'], + region: ['東京'], // Tokyo (should really be 東京都 for Tokyo-to) + region_a: ['SZ'], + country: ['日本'], + country_a: ['JPN'], + continent: ['アジア'], + }; + + const expected = '' + // no postalcode for this record + '東京' + // prefecture (region), tokyo + '静岡' + // city (county), Shizuoka + '静岡市' + // designated city (locality), Shizuoka-shi + '中原' + // chome/ward (street), Nakahara + '331番8号'; // "bango" (block and building number) + t.equal(generator(doc, 'JPN'), expected); + t.end(); + }); + + test('address in Sapporo from OA', function(t) { + const doc = { + name: { default: '2-12 北二十四条西四丁目' }, + layer: 'address', + housenumber: '2-12', + street: '北二十四条西四丁目', + locality: ['札幌市'], + county: ['札幌'], + region: ['北海'], + region_a: ['HK'], + country: ['日本'], + country_a: ['JPN'], + continent: ['アジア'], + }; + const expected = '' + // no postalcode for this record + '北海' + // prefecture (region), Hokkaido Prefecture + '札幌' + // city (county), Sapporo + '札幌市' + // designated city (locality), Sapporo-shi + '北二十四条西四丁目' + // chome/ward (street) + '2番12号'; // "bango" (block and building number) + + t.equal(generator(doc, 'JPN'), expected); + t.end(); + }); + + test('address outside of a designated city', function(t) { + const doc = { + name: { default: '1059-2 大崎' }, + layer: 'address', + housenumber: '1059-2', + street: '大崎', + county: ['小郡市'], + region: ['福岡'], + region_a: ['FO'], + country: ['日本'], + country_a: ['JPN'], + continent: ['アジア'], + }; + + const expected = '' + // no postalcode for this record + '福岡' + // prefecture (region), Fukuoka Prefecture + '小郡市' + // city (county), Ogori + '大崎' + // chome/ward (street), Osaki + '1059番2号'; // "bango" (block and building number) + t.equal(generator(doc, 'JPN'), expected); + t.end(); + }); + + test('neighbourhood admin area', function(t) { + const doc = { + name: { default: '9丁目' }, + layer: 'neighbourhood', + neighbourhood: ['9丁目'], + locality: ['世田谷区'], + county: ['世田谷区'], + region: ['東京'], + region_a: ['TK'], + country: ['日本'], + country_a: ['JPN'], + continent: ['アジア'], + }; + + const expected = '東京' + // prefecture (region), Tokyo + '世田谷区' + // city (county), Setagaya + '世田谷区' + // designated city (locality), Setagaya + '9丁目'; // ward (neighbourhoood) + + t.equal(generator(doc, 'JPN'), expected); + t.end(); + }); +}; + +module.exports.all = function (tape, common) { + function test(name, testFunction) { + return tape('label generator (JPN, 日本ese language): ' + name, testFunction); + } + + for( let testCase in module.exports.tests ){ + module.exports.tests[testCase](test, common); + } +}; diff --git a/test/test.js b/test/test.js index fc4d8a7..008e632 100644 --- a/test/test.js +++ b/test/test.js @@ -17,6 +17,7 @@ var tests = [ require ('./labelGenerator_USA'), require ('./labelGenerator_KOR'), require ('./labelGenerator_JPN'), + require ('./labelGenerator_JPN_JPN'), require ('./labelGenerator_FRA'), require ('./getSchema'), require ('./labelSchema')