From d7f98ab3d2c436885b2ed73ff335b803cbe60cc3 Mon Sep 17 00:00:00 2001 From: Scott Berkley Date: Thu, 19 Dec 2024 17:12:29 -0800 Subject: [PATCH 1/4] format FareProduct amount as currency When exporting to CSV, FareProduct amount should use the appropriate number of decimal places based on the currency specified. Introduces FareAmountFieldMappingFactory to do so. --- .../schema/DecimalFieldMappingFactory.java | 13 +- .../onebusaway/gtfs/model/FareProduct.java | 3 +- .../FareAmountFieldMappingFactory.java | 83 ++++++++++++ .../serialization/FareProductWriterTest.java | 121 +++++++++++++++++ .../FareAmountFieldMappingFactoryTest.java | 128 ++++++++++++++++++ 5 files changed, 346 insertions(+), 2 deletions(-) create mode 100644 onebusaway-gtfs/src/main/java/org/onebusaway/gtfs/serialization/mappings/FareAmountFieldMappingFactory.java create mode 100644 onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/FareProductWriterTest.java create mode 100644 onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/mappings/FareAmountFieldMappingFactoryTest.java diff --git a/onebusaway-csv-entities/src/main/java/org/onebusaway/csv_entities/schema/DecimalFieldMappingFactory.java b/onebusaway-csv-entities/src/main/java/org/onebusaway/csv_entities/schema/DecimalFieldMappingFactory.java index 8f73f608..7ddc0b64 100644 --- a/onebusaway-csv-entities/src/main/java/org/onebusaway/csv_entities/schema/DecimalFieldMappingFactory.java +++ b/onebusaway-csv-entities/src/main/java/org/onebusaway/csv_entities/schema/DecimalFieldMappingFactory.java @@ -24,6 +24,7 @@ import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; +import java.util.Currency; import java.util.Locale; import java.util.Map; @@ -37,6 +38,8 @@ public class DecimalFieldMappingFactory implements FieldMappingFactory { private Locale _locale = Locale.US; + private Currency _currency; + public DecimalFieldMappingFactory() { } @@ -50,6 +53,10 @@ public DecimalFieldMappingFactory(String format, Locale locale) { _locale = locale; } + public DecimalFieldMappingFactory(Currency currency) { + _currency = currency; + } + @Override public FieldMapping createFieldMapping(EntitySchemaFactory schemaFactory, Class entityType, String csvFieldName, String objFieldName, @@ -63,7 +70,11 @@ public FieldMapping createFieldMapping(EntitySchemaFactory schemaFactory, private NumberFormat getFormat(Class entityType, String objFieldName) { String format = determineFormat(entityType, objFieldName); - if (_locale == null) { + if (_currency != null) { + NumberFormat currFormatter = NumberFormat.getCurrencyInstance(Locale.US); + currFormatter.setCurrency(_currency); + return currFormatter; + } else if (_locale == null) { return new DecimalFormat(format); } else { return new DecimalFormat(format, new DecimalFormatSymbols(_locale)); diff --git a/onebusaway-gtfs/src/main/java/org/onebusaway/gtfs/model/FareProduct.java b/onebusaway-gtfs/src/main/java/org/onebusaway/gtfs/model/FareProduct.java index c3545fc0..bb08c712 100644 --- a/onebusaway-gtfs/src/main/java/org/onebusaway/gtfs/model/FareProduct.java +++ b/onebusaway-gtfs/src/main/java/org/onebusaway/gtfs/model/FareProduct.java @@ -21,6 +21,7 @@ import org.onebusaway.gtfs.serialization.mappings.DefaultAgencyIdFieldMappingFactory; import org.onebusaway.gtfs.serialization.mappings.EntityFieldMappingFactory; import org.onebusaway.gtfs.serialization.mappings.FareProductFieldMappingFactory; +import org.onebusaway.gtfs.serialization.mappings.FareAmountFieldMappingFactory; @CsvFields(filename = "fare_products.txt", required = false) public final class FareProduct extends IdentityBean { @@ -32,7 +33,7 @@ public final class FareProduct extends IdentityBean { private AgencyAndId fareProductId; @CsvField(optional = true, name = "fare_product_name") private String name; - @CsvField + @CsvField(name = "amount", optional = false, mapping = FareAmountFieldMappingFactory.class) private float amount = MISSING_VALUE; @CsvField private String currency; diff --git a/onebusaway-gtfs/src/main/java/org/onebusaway/gtfs/serialization/mappings/FareAmountFieldMappingFactory.java b/onebusaway-gtfs/src/main/java/org/onebusaway/gtfs/serialization/mappings/FareAmountFieldMappingFactory.java new file mode 100644 index 00000000..011099ab --- /dev/null +++ b/onebusaway-gtfs/src/main/java/org/onebusaway/gtfs/serialization/mappings/FareAmountFieldMappingFactory.java @@ -0,0 +1,83 @@ +/** + * Copyright (C) 2024 Sound Transit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.onebusaway.gtfs.serialization.mappings; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.util.Currency; +import java.util.Locale; +import java.util.Map; + +import org.onebusaway.csv_entities.CsvEntityContext; +import org.onebusaway.csv_entities.exceptions.CsvEntityException; +import org.onebusaway.csv_entities.schema.AbstractFieldMapping; +import org.onebusaway.csv_entities.schema.BeanWrapper; +import org.onebusaway.csv_entities.schema.EntitySchemaFactory; +import org.onebusaway.csv_entities.schema.FieldMapping; +import org.onebusaway.csv_entities.schema.FieldMappingFactory; + +public class FareAmountFieldMappingFactory implements FieldMappingFactory { + + + public FieldMapping createFieldMapping(EntitySchemaFactory schemaFactory, + Class entityType, String csvFieldName, String objFieldName, + Class objFieldType, boolean required) { + + return new FareAmountFieldMapping(entityType, csvFieldName, objFieldName, required); + } + + private static class FareAmountFieldMapping extends AbstractFieldMapping { + + public FareAmountFieldMapping(Class entityType, String csvFieldName, + String objFieldName, boolean required) { + super(entityType, csvFieldName, objFieldName, required); + } + + @Override + public void translateFromObjectToCSV(CsvEntityContext context, + BeanWrapper object, Map csvValues) { + + String currencyString = (String) object.getPropertyValue("currency"); + Currency currency = Currency.getInstance(currencyString); + Float amount = (Float) object.getPropertyValue(_objFieldName); + + DecimalFormat formatter = (DecimalFormat) NumberFormat.getCurrencyInstance(Locale.US); + formatter.setCurrency(currency); + + // remove "$", "¥", "₹" and other currency symbols from the output + DecimalFormatSymbols symbols = formatter.getDecimalFormatSymbols(); + symbols.setCurrencySymbol(""); + formatter.setDecimalFormatSymbols(symbols); + formatter.setMaximumFractionDigits(currency.getDefaultFractionDigits()); + + csvValues.put(_csvFieldName, formatter.format(amount)); + } + + @Override + public void translateFromCSVToObject(CsvEntityContext context, Map csvValues, BeanWrapper object) + throws CsvEntityException { + + if (isMissingAndOptional(csvValues)) + return; + + Object value = csvValues.get(_csvFieldName); + + Float amount = (float) value; + object.setPropertyValue(_objFieldName, amount); + } + } +} diff --git a/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/FareProductWriterTest.java b/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/FareProductWriterTest.java new file mode 100644 index 00000000..c6d49038 --- /dev/null +++ b/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/FareProductWriterTest.java @@ -0,0 +1,121 @@ +/** + * Copyright (C) 2024 Sound Transit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.onebusaway.gtfs.serialization; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.util.Scanner; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onebusaway.gtfs.impl.FileSupport; +import org.onebusaway.gtfs.impl.GtfsRelationalDaoImpl; +import org.onebusaway.gtfs.model.AgencyAndId; +import org.onebusaway.gtfs.model.FareProduct; + +public class FareProductWriterTest { + private FileSupport _support = new FileSupport(); + private File _tmpDirectory; + + @BeforeEach + public void setup() throws IOException { + _tmpDirectory = File.createTempFile("FareProductWriterTest-", "-tmp"); + if (_tmpDirectory.exists()) + _support.deleteFileRecursively(_tmpDirectory); + _tmpDirectory.mkdirs(); + _support.markForDeletion(_tmpDirectory); + } + + @AfterEach + public void teardown() { + _support.cleanup(); + } + + @Test + public void testWriteAmountWithCorrectDecimalPlaces() throws IOException { + + GtfsWriter writer = new GtfsWriter(); + writer.setOutputLocation(_tmpDirectory); + + FareProduct fp = new FareProduct(); + String agencyId = "a1"; + String fareProductId = "fp1"; + AgencyAndId fpAgencyAndId = new AgencyAndId(agencyId, fareProductId); + float floatCurrency = 4.7f; + String formattedCurrency = "4.70"; + + fp.setId(fpAgencyAndId); + fp.setFareProductId(fpAgencyAndId); + fp.setCurrency("USD"); + fp.setAmount(floatCurrency); + + writer.handleEntity(fp); + + writer.close(); + + GtfsReader reader = new GtfsReader(); + reader.setDefaultAgencyId(agencyId); + reader.setInputLocation(_tmpDirectory); + + Scanner scan = new Scanner(new File(_tmpDirectory + "/fare_products.txt")); + + Boolean onHeader = true; + Boolean containsExpected = false; + while(scan.hasNext()){ + String line = scan.nextLine(); + if (onHeader) { + onHeader = false; + } else { + if (line.contains(formattedCurrency)) { + containsExpected = true; + } + } + } + scan.close(); + + assertTrue(containsExpected, "Line in fare_products.txt did not contain formatted currency amount 4.70"); + + GtfsRelationalDaoImpl dao = new GtfsRelationalDaoImpl(); + reader.setEntityStore(dao); + + reader.readEntities(FareProduct.class); + + FareProduct fareProductFromFile = dao.getAllFareProducts().iterator().next(); + + assertEquals(fareProductFromFile.getAmount(), floatCurrency); + } + + public static void deleteFileRecursively(File file) { + + if (!file.exists()) + return; + + if (file.isDirectory()) { + File[] files = file.listFiles(); + if (files != null) { + for (File child : files) + deleteFileRecursively(child); + } + } + + file.delete(); + } +} diff --git a/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/mappings/FareAmountFieldMappingFactoryTest.java b/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/mappings/FareAmountFieldMappingFactoryTest.java new file mode 100644 index 00000000..8ff63759 --- /dev/null +++ b/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/mappings/FareAmountFieldMappingFactoryTest.java @@ -0,0 +1,128 @@ +/** + * Copyright (C) 2024 Sound Transit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.onebusaway.gtfs.serialization.mappings; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onebusaway.csv_entities.CsvEntityContextImpl; +import org.onebusaway.csv_entities.schema.BeanWrapperFactory; +import org.onebusaway.csv_entities.schema.DefaultEntitySchemaFactory; +import org.onebusaway.csv_entities.schema.DefaultFieldMapping; +import org.onebusaway.csv_entities.schema.FieldMapping; +import org.onebusaway.gtfs.model.FareProduct; +import org.onebusaway.gtfs.serialization.GtfsEntitySchemaFactory; + +public class FareAmountFieldMappingFactoryTest { + + private FieldMapping _fieldMapping; + + @BeforeEach + public void before() { + _fieldMapping = buildFieldMapping(); + } + + @Test + public void testTranslateFromCSVToObject() { + Map csvValues = new HashMap(); + csvValues.put("amount", 47.1234f); + csvValues.put("currency", "USD"); + FareProduct fp = new FareProduct(); + _fieldMapping.translateFromCSVToObject(new CsvEntityContextImpl(), csvValues, BeanWrapperFactory.wrap(fp)); + assertEquals(47.1234, fp.getAmount(), 0.00001); + } + + @Test + public void testTranslateFromObjectToCSV() { + FareProduct fp = new FareProduct(); + fp.setAmount(4.7f); + fp.setCurrency("USD"); + Map csvValues = new HashMap(); + + _fieldMapping.translateFromObjectToCSV(new CsvEntityContextImpl(), BeanWrapperFactory.wrap(fp), csvValues); + assertEquals("4.70", csvValues.get("amount")); + } + + @Test + public void testTranslateFromObjectToCSVCents() { + FareProduct fp = new FareProduct(); + fp.setAmount(4.75f); + fp.setCurrency("USD"); + Map csvValues = new HashMap(); + + _fieldMapping.translateFromObjectToCSV(new CsvEntityContextImpl(), BeanWrapperFactory.wrap(fp), csvValues); + assertEquals("4.75", csvValues.get("amount")); + } + + @Test + public void testTranslateFromObjectToCSVCentsAndMore() { + FareProduct fp = new FareProduct(); + fp.setAmount(4.123f); + fp.setCurrency("USD"); + Map csvValues = new HashMap(); + + _fieldMapping.translateFromObjectToCSV(new CsvEntityContextImpl(), BeanWrapperFactory.wrap(fp), csvValues); + assertEquals("4.12", csvValues.get("amount")); + } + + @Test + public void testTranslateFromObjectToCSVNonUSD() { + FareProduct fp = new FareProduct(); + fp.setAmount(0.7f); + fp.setCurrency("EUR"); + Map csvValues = new HashMap(); + + _fieldMapping.translateFromObjectToCSV(new CsvEntityContextImpl(), BeanWrapperFactory.wrap(fp), csvValues); + assertEquals("0.70", csvValues.get("amount")); + } + + @Test + public void testTranslateFromObjectToCSVWhole() { + FareProduct fp = new FareProduct(); + fp.setAmount(4f); + fp.setCurrency("USD"); + Map csvValues = new HashMap(); + + _fieldMapping.translateFromObjectToCSV(new CsvEntityContextImpl(), BeanWrapperFactory.wrap(fp), csvValues); + assertEquals("4.00", csvValues.get("amount")); + } + + @Test + public void testTranslateFromObjectToCSVNoDecimal() { + FareProduct fp = new FareProduct(); + fp.setAmount(4f); + fp.setCurrency("VND"); + Map csvValues = new HashMap(); + + _fieldMapping.translateFromObjectToCSV(new CsvEntityContextImpl(), BeanWrapperFactory.wrap(fp), csvValues); + assertEquals("4", csvValues.get("amount")); + + fp.setAmount(5.2f); + _fieldMapping.translateFromObjectToCSV(new CsvEntityContextImpl(), BeanWrapperFactory.wrap(fp), csvValues); + assertEquals("5", csvValues.get("amount"), "Amount did not get rounded to nearest whole number for currency that doesn't use decimals"); + } + + private FieldMapping buildFieldMapping() { + FareAmountFieldMappingFactory factory = new FareAmountFieldMappingFactory(); + DefaultEntitySchemaFactory schemaFactory = GtfsEntitySchemaFactory.createEntitySchemaFactory(); + return factory.createFieldMapping(schemaFactory, FareProduct.class, "amount", + "amount", Float.TYPE, true); + } +} From 39c849a6016e2319dc4e086ad74e191a2b6a1dfb Mon Sep 17 00:00:00 2001 From: Scott Berkley Date: Thu, 9 Jan 2025 15:25:01 -0800 Subject: [PATCH 2/4] Update FareAmountFieldMappingFactory.java explicitly cast fare amount from CSV to string, then float --- .../serialization/mappings/FareAmountFieldMappingFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onebusaway-gtfs/src/main/java/org/onebusaway/gtfs/serialization/mappings/FareAmountFieldMappingFactory.java b/onebusaway-gtfs/src/main/java/org/onebusaway/gtfs/serialization/mappings/FareAmountFieldMappingFactory.java index 011099ab..0f499787 100644 --- a/onebusaway-gtfs/src/main/java/org/onebusaway/gtfs/serialization/mappings/FareAmountFieldMappingFactory.java +++ b/onebusaway-gtfs/src/main/java/org/onebusaway/gtfs/serialization/mappings/FareAmountFieldMappingFactory.java @@ -76,7 +76,7 @@ public void translateFromCSVToObject(CsvEntityContext context, Map Date: Thu, 9 Jan 2025 15:47:46 -0800 Subject: [PATCH 3/4] Update FareAmountFieldMappingFactoryTest.java use string in CSV to object tests --- .../FareAmountFieldMappingFactoryTest.java | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/mappings/FareAmountFieldMappingFactoryTest.java b/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/mappings/FareAmountFieldMappingFactoryTest.java index 8ff63759..921ebcf9 100644 --- a/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/mappings/FareAmountFieldMappingFactoryTest.java +++ b/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/mappings/FareAmountFieldMappingFactoryTest.java @@ -25,7 +25,6 @@ import org.onebusaway.csv_entities.CsvEntityContextImpl; import org.onebusaway.csv_entities.schema.BeanWrapperFactory; import org.onebusaway.csv_entities.schema.DefaultEntitySchemaFactory; -import org.onebusaway.csv_entities.schema.DefaultFieldMapping; import org.onebusaway.csv_entities.schema.FieldMapping; import org.onebusaway.gtfs.model.FareProduct; import org.onebusaway.gtfs.serialization.GtfsEntitySchemaFactory; @@ -42,11 +41,41 @@ public void before() { @Test public void testTranslateFromCSVToObject() { Map csvValues = new HashMap(); - csvValues.put("amount", 47.1234f); + csvValues.put("amount", "47.12"); csvValues.put("currency", "USD"); FareProduct fp = new FareProduct(); _fieldMapping.translateFromCSVToObject(new CsvEntityContextImpl(), csvValues, BeanWrapperFactory.wrap(fp)); - assertEquals(47.1234, fp.getAmount(), 0.00001); + assertEquals(47.12, fp.getAmount(), 0.001); + } + + @Test + public void testTranslateFromCSVToObjectWhole() { + Map csvValues = new HashMap(); + csvValues.put("amount", "47"); + csvValues.put("currency", "USD"); + FareProduct fp = new FareProduct(); + _fieldMapping.translateFromCSVToObject(new CsvEntityContextImpl(), csvValues, BeanWrapperFactory.wrap(fp)); + assertEquals(47, fp.getAmount(), 0.001); + } + + @Test + public void testTranslateFromCSVToObjectWholeDecimals() { + Map csvValues = new HashMap(); + csvValues.put("amount", "47.00"); + csvValues.put("currency", "USD"); + FareProduct fp = new FareProduct(); + _fieldMapping.translateFromCSVToObject(new CsvEntityContextImpl(), csvValues, BeanWrapperFactory.wrap(fp)); + assertEquals(47, fp.getAmount(), 0.001); + } + + @Test + public void testTranslateFromCSVToObjectNonUSD() { + Map csvValues = new HashMap(); + csvValues.put("amount", "47"); + csvValues.put("currency", "JPY"); + FareProduct fp = new FareProduct(); + _fieldMapping.translateFromCSVToObject(new CsvEntityContextImpl(), csvValues, BeanWrapperFactory.wrap(fp)); + assertEquals(47, fp.getAmount(), 0.001); } @Test From b42367729e55486c27217ce52949876dc7ed6050 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Fri, 10 Jan 2025 07:01:32 +0100 Subject: [PATCH 4/4] Update onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/FareProductWriterTest.java --- .../onebusaway/gtfs/serialization/FareProductWriterTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/FareProductWriterTest.java b/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/FareProductWriterTest.java index c6d49038..536a2dd4 100644 --- a/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/FareProductWriterTest.java +++ b/onebusaway-gtfs/src/test/java/org/onebusaway/gtfs/serialization/FareProductWriterTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2024 Sound Transit + * Copyright (C) 2025 Sound Transit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.