/*
* [PrepStateBase.java]
*
* Summary: Collects common code for various program that prepare data for the AmericanTax.java table.
*
* Copyright: (c) 2010-2017 Roedy Green, Canadian Mind Products, http://mindprod.com
*
* Licence: This software may be copied and used freely for any purpose but military.
* http://mindprod.com/contact/nonmil.html
*
* Requires: JDK 1.8+
*
* Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/
*
* Version History:
* 1.0 2010-12-11 initial,
*/
package com.mindprod.americantax;
import com.mindprod.common18.Build;
import com.mindprod.common18.EIO;
import com.mindprod.common18.ST;
import com.mindprod.csv.CSVReader;
import com.mindprod.csv.CSVWriter;
import java.io.BufferedReader;
import java.io.EOFException;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import static java.lang.System.*;
/**
* Collects common code for various program that prepare data for the AmericanTax.java table.
*
* S
*
* @author Roedy Green, Canadian Mind Products
* @version 1.0 2010-12-11 initial,
* @since 2010-12-11
*/
public class PrepStateBase
{
/**
* track county city percent
*/
final ArrayList salesTaxItems;
/**
* percent state wide sales tax
*/
final double stateTax;
/**
* 2-letter state abbr
*/
private final String stateAbbr;
/**
* lower case state name
*/
private final String stateName;
/**
* true if county/city should be converted to book case
*/
private final boolean convertToBookCase;
/**
* true if read a file of cities (may generate some cities as a side effect)
*/
private final boolean hasCities;
// --Commented out by Inspection START (17/12/10 9:08 PM):
// /**
// * true if if should guess country rates as lowest of city rates in a county, if no county rate
// */
// final boolean shouldGuessCountryRates;
// --Commented out by Inspection STOP (17/12/10 9:08 PM)
/**
* true if read a file of counties (may generate some cities as a side effect)
*/
private final boolean hasCounties;
/**
* true if county and city rates in raw files include state tax. We then must strip it out
*/
private final boolean stateTaxIncluded;
/**
* Constructor
*
* @param stateAbbr 2-letter state abbr, upper case e.g. WY
* @param stateName lower case e.g. wyoming, no spaces.
* @param stateTax percent statewide sales tax. Must also record in statetax.csv.
* @param hasCounties true if we read a file of county data
* @param hasCities true if we read a file of city data.
* @param stateTaxIncluded true if state tax is included in files we process
* @param convertToBookCase true if want county and city names converted to book case (are all caps now).
* @param estItems estimated number of items county/city/percent we will produce.
*/
PrepStateBase(
final String stateAbbr,
final String stateName,
final double stateTax,
final boolean hasCounties,
final boolean hasCities,
final boolean stateTaxIncluded,
final boolean convertToBookCase,
final int estItems )
{
this.stateAbbr = stateAbbr;
this.stateName = stateName.toLowerCase();
this.stateTax = stateTax;
this.hasCounties = hasCounties;
this.hasCities = hasCities;
this.stateTaxIncluded = stateTaxIncluded;
this.convertToBookCase = convertToBookCase;
salesTaxItems = new ArrayList<>( estItems );
}
/**
* remove entries where all three fields are identical
*/
void dedup()
{
// at this point there is one entry per city and one per county
Collections.sort( salesTaxItems, new SalesTaxItem.Alphabetically() );
// remove utter duplicates
SalesTaxItem prev = new SalesTaxItem( "dummy", "dummy", 0 );
// don't convert to for:each we need iter.remove.
for ( Iterator iter = salesTaxItems.iterator(); iter.hasNext(); )
{
SalesTaxItem salesTaxItem = iter.next();
if ( prev.getCounty().equals( salesTaxItem.getCounty() )
&& prev.getPercent() == salesTaxItem.getPercent() && prev.getCity()
.equals( salesTaxItem.getCity() ) )
{
// same as prev, we don't need this entry. Remove without fanfare.
iter.remove();
}
prev = salesTaxItem;
} // end for
}
/**
* detect duplicate county-cities, possibly different rates.
*/
void detectConflictingRates()
{
Collections.sort( salesTaxItems, new SalesTaxItem.Alphabetically() );
// detect duplicated city
SalesTaxItem prev = new SalesTaxItem( "dummy", "dummy", 0 );
for ( SalesTaxItem item : salesTaxItems )
{
if ( prev.getCounty().equals( item.getCounty() )
&& prev.getCity().equals( item.getCity() ) )
{
err.println( "Duplicate: County: "
+ item.getCounty()
+ ", city: "
+ item.getCity()
+ ", tax: "
+ prev.getPercent()
+ ", inconsistent tax: "
+ item.getPercent() );
// keep going.
}
prev = item;
} // end for
}
/**
* export the prepared data to the xxxdistricts.csv file
*/
void export()
{
final String exportFilename = Build.MINDPROD_SOURCE + "/americantax/" + stateName + "districts.csv";
try
{
CSVWriter w = new CSVWriter( EIO.getPrintWriter( new File( exportFilename ), 1024 * 4, EIO.UTF8 ) );
// put out state sales tax only dummy item.
w.put( stateAbbr );
w.put( "" );
w.put( "" );
w.put( 0.0, 4 );
w.nl();
for ( SalesTaxItem salesTaxItem : salesTaxItems )
{
// like this: XX ,Roane, Oliver Springs, 2.7500
w.put( stateAbbr );
w.put( salesTaxItem.getCounty() );
w.put( salesTaxItem.getCity() );
w.put( salesTaxItem.getPercent(), 4 );
w.nl();
}
w.close();
}
catch ( IOException e )
{
/* done */
err.println( "can't write " + exportFilename );
System.exit( 1 );
}
}
/**
* If no country record, create one as lowest of cities in that county.
*/
void guessCountyRates()
{
// at this point there is one entry per city and one per county
Collections.sort( salesTaxItems, new SalesTaxItem.Alphabetically() );
String county = "";
boolean missing = false;
double lowestCityPercent = 0;
// addition county records we guess
final ArrayList guessed = new ArrayList<>( 200 );
for ( SalesTaxItem salesTaxItem : salesTaxItems )
{
final String city = salesTaxItem.getCity();
if ( city.length() == 0 )
{
// finish any pending missing
if ( missing )
{
out.println( "adding: " + county + " " + lowestCityPercent );
guessed.add( new SalesTaxItem( county, "", lowestCityPercent ) );
}
// this is a county-only record. Save the rate
county = salesTaxItem.getCounty();
missing = false;
}
else
{
// was a city.
if ( county.equals( salesTaxItem.getCounty() ) )
{
// same county as previous county or city
if ( missing )
{
// see if we have a lower rate
final double cityPercent = salesTaxItem.getPercent();
if ( cityPercent < lowestCityPercent )
{
lowestCityPercent = cityPercent;
}
}
else
{
/* not missing, ignore this same as prev */
}
}
else
{
// we have a city with a new county
// finish off previous
if ( missing )
{
out.println( "adding: " + county + " " + lowestCityPercent );
guessed.add( new SalesTaxItem( county, "", lowestCityPercent ) );
}
county = salesTaxItem.getCounty();
missing = true;
lowestCityPercent = salesTaxItem.getPercent();
}
}
}
if ( missing )
{
out.println( "adding: " + county + " " + lowestCityPercent );
guessed.add( new SalesTaxItem( county, "", lowestCityPercent ) );
}
// merge our guesses in at the end.
salesTaxItems.addAll( guessed );
Collections.sort( salesTaxItems, new SalesTaxItem.Alphabetically() );
}
/**
* prepare counties, cities, validate and export
*/
void prepare()
{
out.println( "Preparing tax data for " + stateAbbr + " " + stateName );
if ( hasCounties )
{
prepareCounties();
}
if ( hasCities )
{
prepareCities();
}
dedup();
detectConflictingRates();
// pruneCities(); // prune cities with same rate as county. They are not exceptions.
export();
}
/**
* default code to prepare counties by reading a csv file.
*/
void prepareCities()
{
final String countyFilename = Build.MINDPROD_SOURCE + "/americantax/" + stateName + "cities.csv";
try
{
// read county and city
CSVReader c = new CSVReader( new BufferedReader( new FileReader( countyFilename ) ) );
try
{
while ( true )
{
prepareCity( c );
}
}
catch ( EOFException e )
{
c.close();
}
}
catch ( IOException e )
{
/* done */
err.println( "can't read " + countyFilename );
System.exit( 1 );
}
}
/**
* Default method to read and prepare one county record.
* You must read, and add SalesTaxItem to salesTaxItems, and skipToNewLine
*
* @param c open CSV reader.
*
* @throws IOException if cannot read file
*/
void prepareCity( final CSVReader c ) throws IOException
{
final String county = convertToBookCase ? ST.toBookTitleCase( c.get() ) : c.get();
final String city = convertToBookCase ? ST.toBookTitleCase( c.get() ) : c.get();
final double percent = c.getDouble() - ( stateTaxIncluded ? stateTax : 0 );
c.skipToNextLine();
salesTaxItems.add( new SalesTaxItem( county, city, percent ) );
}
/**
* default code to prepare counties by reading a csv file.
*/
void prepareCounties()
{
final String countyFilename = Build.MINDPROD_SOURCE + "/americantax/" + stateName + "counties.csv";
try
{
// read counties and county tax
CSVReader c = new CSVReader( new BufferedReader( new FileReader( countyFilename ) ) );
try
{
while ( true )
{
prepareCounty( c );
}
}
catch ( EOFException e )
{
c.close();
}
}
catch ( IOException e )
{
/* done */
err.println( "can't read " + countyFilename );
System.exit( 1 );
}
}
/**
* Default method to read and prepare one county record.
* You must read, and add SalesTaxItem to salesTaxItems, and skipToNewLine
*
* @param c open CSV reader.
*
* @throws IOException if cannot read file
*/
void prepareCounty( final CSVReader c ) throws IOException
{
final String countyName = convertToBookCase ? ST.toBookTitleCase( c.get() ) : c.get();
final double percent = c.getDouble() - ( stateTaxIncluded ? stateTax : 0 );
c.skipToNextLine();
salesTaxItems.add( new SalesTaxItem( countyName, "", percent ) );
}
// --Commented out by Inspection START (17/12/10 9:08 PM):
// /**
// * drop item for city if it has the same tax rate as the enclosing county.
// */
// void pruneCities()
// {
// // at this point there is one entry per city and one per county
// Collections.sort( salesTaxItems, new SalesTaxItem.Alphabetically() );
//
// String county = "";
// double countyPercent = 0;
// // don't convert to for:each we need iter.remove.
// for ( Iterator iter = salesTaxItems.iterator(); iter.hasNext(); )
//
// {
// SalesTaxItem salesTaxItem = iter.next();
//
// final String city = salesTaxItem.getCity();
// if ( city.length() == 0 )
// {
// // this is a county-only record. Save the rate
// county = salesTaxItem.getCounty();
// countyPercent = salesTaxItem.getPercent();
// }
// else
// {
// // was a city. See if we can toss it.
// if ( county.equals( salesTaxItem.getCounty() ) && countyPercent == salesTaxItem.getPercent() )
// {
// // this city in the county has the same rate as the county. We can toss it.
// out.println( "pruning: " + county + " " + city + " " + countyPercent );
// iter.remove();
// }
// }
// } // end for
// }
// --Commented out by Inspection STOP (17/12/10 9:08 PM)
@SuppressWarnings( { "InfiniteLoopStatement" } )
public static void main( String[] args ) throws IOException
{
// example of use. Override methods as needed.
new PrepStateBase(
"WY",
"wyoming",
4.0,
true /* counties */,
true /* cities */,
true /* files include state rate */,
false /* no convert to book case */,
/* guess country rates */
600 ).prepare();
}
}