/* * [CurrCon.java] * * Summary: Currency converter Applet. Displays amount in any international currency. * * Copyright: (c) 2001-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 2001-03-08 initial * 1.1 2001-03-20 use Exch objects for initialisation. * 1.2 2001-12-20 catch missing params * 1.3 2002-03-07 error message for unrecognised currency, * bad amount. * update exchange rates * fix bug so now all Applets don't necessarily share same country. * 1.4 2002-03-27 list of exchanges comes in serialised form, easier to update daily without * a recompile. * 1.5 2002-04-01 guess favoured currency. * avoids using Currency class which requires JDK 1.4 * 1.6 2002-04-24 hashtable for country lookup. * show=cCN$A, only create widgets needed. * display symbol * 1.7 2002-05-14 display large numbers in millions. * widths made public for com.mindprod.htmlmacros.CurrCon * 1.8 2003-11-01 get Bank of Canada Feed. Automatically delete countries without data * today. * 1.9 2004-07-07 remove rounding for 7 M style display since format has it built-in. * 2.0 2005-06-13 add Trillions, Show millions and billions to one decimal point. * name all colours * 2.1 2005-07-16 set up with standard bat files. * ensure compiled under 1.1 under all circumstances * 2.2 2005-08-11 convert non-displaying currency signs to general currency. * 2.3 2006-01-01 * 2.4 2006-02-16 change colour scheme. Currency code not so brazen. * 2.5 2006-03-04 reformat with IntelliJ. add Javadoc. * 2.6 2007-01-29 * 2.7 2008-01-29 * 2.8 2008-01-29 get version number is sync with currConAux * 2.9 2008-01-30 safer code for Applet instance interaction * 3.0 2008-01-31 locks and other means to deal with interApplet fibrillation. * 3.1 2008-01-31 volatile and yield to help smooth interApplet fibrillation. * 3.2 2008-01-31 avoid shared variable to smooth interApplet fibrillation. * 3.3 2008-02-01 use threads and invokeLater to avoid freezes on Linux * 3.4 2008-02-15 lower price. * 3.5 2008-09-19 allow commas in parameter amount values. * 3.6 2010-02-04 new colours to match mindprod style sheet. wider currency code selector to hold KRW * 3.7 2010-05-31 add P code that acts like A, but gives precise value. * 3.8 2010-06-02 ensure P code prices update when currency changed elsewhere on the page. * 3.9 2010-12-04 compress resource. use pure arrays to avoid generics trouble. * 4.0 2011-01-21 new format for BOC files, no longer need boccodes.csv * 4.1 2011-01-26 switch to iso currency codes, allow multi-char currency symbols. Allow accents in currency names. * Flip from JDK 1.1 to 1.5 * 4.2 2011-02-19 now supports Windows/IE variable resolution. * 4.3 2011-05-07 adjust to new Boc Format, URLs and filenames. * 4.4 2011-05-18 adjust to new format of Boc exchange rates. Simplify by eliminating currconaux directory. * 4.5 2011-12-29 adjust to new format from Bank of Canada. */ /* TODO - use java.util.Currency instead of our own currency tables. - autoresizing Applets. - L code for large amount field that is left exact. - allow commas/dots in amount field. - store currency preference in cookie.. - automatic update of exchange rates. - sort by country name, also currency abbr. - way to let user know how up to date exchange rates are. - bigger box for bigger value. - optional amount display - smart width, drop decimal points and commas if room tight. Drop show country if room tight. - use smaller font. possible futures n = name of currency, user may change. Applet width and height must be big enough to contain all the pieces you include. c=50 C=30 N=120 $=10 A=70 P=100 c$A is width 130, cN is width 170, $A is width 80, c$AN is 250, $AN is 200 $AC is 110 </applet> */ package com.mindprod.currcon; import com.mindprod.common18.EIO; import com.mindprod.common18.ST; import com.mindprod.common18.VersionCheck; import com.mindprod.fastcat.FastCat; import javax.swing.BorderFactory; import javax.swing.JApplet; import javax.swing.JComboBox; import javax.swing.JLabel; import java.awt.Container; import java.awt.Font; import java.awt.Label; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.io.ObjectInputStream; import java.text.DecimalFormat; import java.util.HashMap; import java.util.Locale; import static java.lang.System.*; /** * Currency converter Applet. Displays amount in any international currency. *

* Uses Applet tags like this: * <applet archive="currcon.jar" code="CurrCon.class" * width="130" height="20" alt="1000.00 CAD" * > * <param name="currency" value="CAD"> * <param name="amount" value="1000.00"> * <param name="show" value="c$A"> * </applet> *

* <applet archive="currcon.jar" code="CurrCon.class" * width="130" height="20" alt="1000.00 CAD" * > * <param name="currency" value="CAD"> * <param name="amount" value="1000.00"> * always with . and no commas or $ despite locale. * max $999,999.99 * <param name="show" value="c$A"> * possible letters: * c = 3-letter code, user may change * C = 3-letter code, user may not change, tracks changes elsewhere. * N = name of the currency, user may not change. * $ = lead currency symbol on value. * A = amount, converted to selected currency. user may not change. * P = precise amount. No abbreviations. * If there is a show value A present, then currency and amount params must be * present. *

* any combinations and orders are possible, though you would not likely * have both c and C. It makes no sense to have $ without V. *

* The key trick is Broadcaster that uses AppletContext.getApplets to find all the other * instances of CurrCon on the same page, and notify them of the change of Currency. * * @author Roedy Green, Canadian Mind Products * @version 4.5 2011-12-29 adjust to new format from Bank of Canada. * @since 2001-03-08 */ public final class CurrCon extends JApplet { // When CurrCon was written, all instances of the Applet on a page ran on a single thread. // Now each instance gets its own thread. private static final int FIRST_COPYRIGHT_YEAR = 2001; /** * undisplayed copyright notice */ private static final String EMBEDDED_COPYRIGHT = "Copyright: (c) 2001-2017 Roedy Green, Canadian Mind Products, http://mindprod.com"; private static final String RELEASE_DATE = "2011-12-29"; /** * embedded version string. */ private static final String VERSION_STRING = "4.5"; /** * true if want extra debugging output on console. * If you turn on debugging, turn on instance parm is com.mindprod.htmlmacros.macro.CurrCon */ static boolean DEBUGGING = false; /** * table to lookup country and get the current it uses. shared by all Applets */ private static HashMap countryToCurr; /** * list of the daily exchange rates for various currencies. shared by all Applets */ private static Exch[] exchs; static { // should happen once, only one and before any init method runs. fetchCurrencyInfo(); } /** * helps format the amount display , for 0 to 3 decimal places DecimalFormat is not thread Safe. That should not * matter since all Applets run on the same thread, but just in case... Actual display is Locale dependent, e.g. * comma or decimal point. Not thread safe so we make them non-static. */ private final DecimalFormat[] df = { new DecimalFormat( "###,###,##0" ), new DecimalFormat( "#,###,##0.0" ), new DecimalFormat( "###,##0.00" ), new DecimalFormat( "###,##0.000" ), }; /** * which instance on the page are we, unique string */ private String instance = "unknown"; /** * font we use mostly. Dialog has good support for currency symbols, except Indian Rupee sign. */ private Font mainFont; /** * listener for user to select a different currency */ private ItemListener theItemListener; /** * allow user to select which 3-letter currency to use. */ private JComboBox currCodeSelect; /** * Display the converted amount. */ private JLabel compactAmountDisplay; /** * display of 3 letter code */ private JLabel currencyCodeDisplay; /** * display the name of the currency in words, e..g. Canadian dollars */ private JLabel currenceNameDisplay; /** * used to display errors */ private JLabel invalidDisplay; /** * Display the converted amount precisely, without abbreviations */ private JLabel preciseAmountDisplay; /** * 3-letter iso currency code for base currency, one described in the Applet tag. */ private String baseCurrencyAbbr; /** * 3-letter ISO currency code. The user's preferred code based on his country setting. Does not change as selects * different codes. */ private String userCurrencyAbbr; /** * true once this instance of the Applet is ready to go */ private boolean ready = false; /** * do we put a lead $ on value */ private boolean showSymbols = false; /** * The amount we want to display in the base currency */ private double baseAmount = 0; /** * actual height of applet, may be higher than we requested on currency and exchange rates */ private static void fetchCurrencyInfo() { try { // O P E N // get resources "/com/mindprod/currcon/exchs.ser" final ObjectInputStream ois = EIO.getObjectInputStream( CurrCon.class.getResourceAsStream( "exchs.ser" ), 4 * 1024, true ); // R E A D long wasResourceVersion = ( Long ) ois.readObject(); if ( wasResourceVersion != Exch.getSerialVersionUID() ) { throw new IllegalArgumentException( "Wrong version of exchs.ser file. Was:" + wasResourceVersion + " wanted:" + Exch.getSerialVersionUID() ); } if ( Exch.getSerialVersionUID() != Geom.serialVersionUID ) { throw new IllegalArgumentException( "Inconsistent Exch and Geom code. Exch:" + Exch .getSerialVersionUID() + " Geom:" + Geom.serialVersionUID ); } final String[] country = ( String[] ) ois.readObject(); final String[] currency = ( String[] ) ois.readObject(); exchs = ( Exch[] ) ois.readObject(); // C L O S E ois.close(); countryToCurr = new HashMap<>( 250 * 130 / 100 ); for ( int i = 0; i < country.length; i++ ) { countryToCurr.put( country[ i ], currency[ i ] ); } } catch ( Exception e ) { err.println( "Missing or damaged exchs.ser " + e + "\n" ); } } /** * Find a given currency abbreviation in the list and return the index of it. * * @param currAbbr 3-letter ISO code for currency * * @return index in exchs list or -1 if not found. */ private static int findCurrencyInfo( String currAbbr ) { /* search for currAbbr in list of currencies */ for ( int i = 0; i < exchs.length; i++ ) { // if for some reason the exchs not fully populated yet treat as // not found if ( exchs[ i ].getCurrAbbr().equals( currAbbr ) ) { return i; } } /* did not find it */ return -1; } /** * adjust to compensate for possibility IE gives us more space that we asked for * * @param size size to adjust * * @return adjusted size */ private int adjust( int size ) { return size * appletHeight / 20; } /** * build the components, just the ones requested, in order. * * @param contentPane contentPane for JApplet * @param whatToShow string of chars indicating what to display. */ private void buildComponents( final Container contentPane, final String whatToShow ) { int width; int x = 0; // decide what to display and where final char[] whatToShowLetters = whatToShow.toCharArray(); for ( char showCommandLetter : whatToShowLetters ) { switch ( showCommandLetter ) { case '$': /* add lead $ */ showSymbols = true;/* part of compactAmountDisplay */ break; case 'A': /* add a display amount */ compactAmountDisplay = new JLabel( "", JLabel.RIGHT ); compactAmountDisplay.setBackground( Scheme.BACKGROUND_FOR_COMPACT_AMOUNT ); compactAmountDisplay.setBorder( BorderFactory.createEmptyBorder( 0 /*top*/, 0 /* left*/, 0 /*bottom */, 2 /*right*/ ) ); compactAmountDisplay.setFont( mainFont ); compactAmountDisplay.setForeground( Scheme.FOREGROUND_FOR_COMPACT_AMOUNT ); compactAmountDisplay.setOpaque( true ); width = Geom.COMPACT_AMOUNT_DISPLAY_WIDTH; if ( showSymbols ) { width += Geom.SYMBOL_DISPLAY_WIDTH; } width = adjust( width ); compactAmountDisplay.setBounds( x, 0, width, appletHeight ); x += width; contentPane.add( compactAmountDisplay ); calcAmounts();// after add, so that font defined. break; case 'c': /* add a currency code selector */ /* currency code selector, may or may not be used */ currCodeSelect = new JComboBox<>(); currCodeSelect.setBackground( Scheme.BACKGROUND_FOR_CURRENCY_SELECTOR ); currCodeSelect.setEditable( false ); currCodeSelect.setForeground( Scheme.FOREGROUND_FOR_CURRENCY_SELECTOR ); currCodeSelect.setOpaque( true ); // add legal currency codes for which we have current data. for ( Exch exch : exchs ) { if ( exch.getExchangeRate() == 0 ) { err.println( "corrupt exchs file contains 0 exchange rate." ); System.exit( 1 ); } currCodeSelect.addItem( exch.getCurrAbbr() ); } // will not generate any events, since listener not hooked up yet. currCodeSelect.setSelectedIndex( currentCurrencyIndex ); width = adjust( Geom.CURR_CODE_SELECT_WIDTH ); currCodeSelect.setBounds( x, 0, width, appletHeight );/* x y width height */ x += width; contentPane.add( currCodeSelect ); theItemListener = new ItemListener() { /** * Listen for any actions from the end-user. * @param event details of what end user just clicked. */ public void itemStateChanged( ItemEvent event ) { if ( event.getStateChange() == ItemEvent.SELECTED ) { //inform yourself and others of the change. int newCurrencyIndex = currCodeSelect.getSelectedIndex(); // don't bother broadcasting unless there really was a change. if ( newCurrencyIndex >= 0 && newCurrencyIndex != currentCurrencyIndex ) { // notify self currencyChangeListener( newCurrencyIndex ); // notify others with a common separate notifying thread so we won't tie up the EDT threat. if ( DEBUGGING ) { // use an instance parameter to help sort out instances out.println( instance + " notifying other applets of change to " + exchs[ newCurrencyIndex ].getCurrAbbr() ); } new Thread( new Broadcaster( CurrCon.this, newCurrencyIndex ) ).start(); } } } }; currCodeSelect.addItemListener( theItemListener ); break; case 'C': /* add a currency code display */ currencyCodeDisplay = new JLabel( userCurrencyAbbr ); currencyCodeDisplay.setBackground( Scheme.BACKGROUND_FOR_CURRENCY_CODE ); currencyCodeDisplay.setBorder( BorderFactory.createEmptyBorder( 0 /*top*/, 2 /* left*/, 0 /*bottom */, 2 /*right*/ ) ); currencyCodeDisplay.setForeground( Scheme.FOREGROUND_FOR_CURRENCY_CODE ); currencyCodeDisplay.setOpaque( true ); width = adjust( Geom.CURR_CODE_DISPLAY_WIDTH ); currencyCodeDisplay.setBounds( x, 0, width, appletHeight );/* x y width height */ x += width; contentPane.add( currencyCodeDisplay ); break; case 'N': /* add a currency name display */ currenceNameDisplay = new JLabel( exchs[ currentCurrencyIndex ].getCurrName() ); currenceNameDisplay.setBackground( Scheme.BACKGROUND_FOR_CURRENCY_NAME ); if ( x > 0 ) { // give a little space after the preceding field currenceNameDisplay.setBorder( BorderFactory.createEmptyBorder( 0 /*top*/, 3 /* left*/, 0 /*bottom */, 0 /*right*/ ) ); } currenceNameDisplay.setForeground( Scheme.FOREGROUND_FOR_CURRENCY_NAME ); currenceNameDisplay.setOpaque( true ); width = adjust( Geom.CURR_NAME_DISPLAY_WIDTH ); currenceNameDisplay.setBounds( x, 0, width, appletHeight ); x += width; contentPane.add( currenceNameDisplay ); break; case 'P': /* add a precise display amount */ preciseAmountDisplay = new JLabel( "", JLabel.RIGHT ); preciseAmountDisplay.setBackground( Scheme.BACKGROUND_FOR_PRECISE_AMOUNT ); preciseAmountDisplay.setBorder( BorderFactory.createEmptyBorder( 0 /*top*/, 0 /* left*/, 0 /*bottom */, 2 /*right*/ ) ); preciseAmountDisplay.setFont( mainFont ); preciseAmountDisplay.setForeground( Scheme.FOREGROUND_FOR_PRECISE_AMOUNT ); preciseAmountDisplay.setOpaque( true ); width = Geom.PRECISE_AMOUNT_DISPLAY_WIDTH; if ( showSymbols ) { width += Geom.SYMBOL_DISPLAY_WIDTH; } width = adjust( width ); preciseAmountDisplay.setBounds( x, 0, width, appletHeight ); x += width; contentPane.add( preciseAmountDisplay ); calcAmounts();// after add, so that font defined. break; case 'I': default: err.println( "invalid show parameter: " + whatToShow ); invalidDisplay = new JLabel( "err 1" ); invalidDisplay.setBackground( Scheme.BACKGROUND_FOR_INVALID ); invalidDisplay.setForeground( Scheme.FOREGROUND_FOR_INVALID ); invalidDisplay.setOpaque( true ); width = adjust( Geom.INVALID_DISPLAY_WIDTH ); invalidDisplay.setBounds( x, 0, width, appletHeight ); x += width; add( invalidDisplay ); } // end switch } // end for } /** * The guts of the program. Do the exchange rate calculation. What a lot of futzing about just to get to this! * This may be operating on some other CurrCon Applet on the page, where this refers to that Applet. * On entry baseCurrencyIndex (currency of price), currencyIndex (currency of user) and baseAmount must be set. * Formats result and does setText */ private void calcAmounts() { if ( preciseAmountDisplay == null && compactAmountDisplay == null ) { return; } // value of 1 unit of currency in units being displayed. final Exch exch = exchs[ currentCurrencyIndex ]; // base was currency used to specify the original price. final double convertedAmount = baseAmount * exchs[ baseCurrencyIndex ].getExchangeRate() / exch.getExchangeRate(); final int decimalPlaces = exch.getDecimalPlaces(); final String symbol = exch.getSymbol(); if ( preciseAmountDisplay != null ) { calcPreciseAmount( convertedAmount, decimalPlaces, symbol ); } if ( compactAmountDisplay != null ) { calcCompactAmount( convertedAmount, decimalPlaces, symbol ); } } /** * display compact value of amount rounded to even thousands, millions etc.y. * * @param convertedAmount amount is local currency * @param decimalPlaces number of decimal places to display * @param symbol currency symbol. */ private void calcCompactAmount( final double convertedAmount, final int decimalPlaces, final String symbol ) { // if value is not too big, we might be able to pull off precise String convertedAmountText = df[ decimalPlaces ].format( convertedAmount ); if ( convertedAmountText.length() > 17 ) { // if number is too big, display in one-place trillions, no need // to round, format does that automatically. convertedAmountText = df[ 1 ].format( convertedAmount / 1000000000000d ) + " T"; } else if ( convertedAmountText.length() > 14 ) { // if number is too big, display in one-place billions, no need // to round, format does that automatically. convertedAmountText = df[ 1 ].format( convertedAmount / 1000000000d ) + " B"; } else if ( convertedAmountText.length() > 11 ) { // if number is too big, display in one-place millions, no need // to round, format does that automatically. convertedAmountText = df[ 1 ].format( convertedAmount / 1000000d ) + " M"; } final FastCat sb = new FastCat( 2 ); if ( showSymbols ) { sb.append( symbol ); } // end if sb.append( convertedAmountText ); compactAmountDisplay.setText( sb.toString() ); } /** * display precise value of amount to the penny. * * @param convertedAmount amount is local currency * @param decimalPlaces number of decimal places to display * @param symbol currency symbol. */ private void calcPreciseAmount( final double convertedAmount, final int decimalPlaces, final String symbol ) { String convertedAmountText = df[ decimalPlaces ].format( convertedAmount ); final FastCat sb = new FastCat( 2 ); if ( showSymbols ) { sb.append( symbol ); } // end if sb.append( convertedAmountText ); preciseAmountDisplay.setText( sb.toString() ); } /** * notice a broadcast from some other CurrCon Applet on the page that the current country has changed. Synchronized * in ane we get fibrillation and multiple Applets broadcasting. This will be called for OTHER Applets on the * page, so this refers to their Applet instance. Other applets will invoke this on the EDT thread. * * @param newCurrencyIndex which country to select, 0..N */ synchronized void currencyChangeListener( int newCurrencyIndex ) { if ( DEBUGGING ) { // use an instance parameter to help sort out instances out.println( instance + " notified of change to " + exchs[ newCurrencyIndex ].getCurrAbbr() ); } if ( !ready ) { return; } if ( this.currentCurrencyIndex == newCurrencyIndex ) { // work already done return; } this.currentCurrencyIndex = newCurrencyIndex; // any of the Components may be present or absent. if ( currCodeSelect != null ) { // avoid triggering a fission explosion of broadcasts from ItemStateChangedEvents. Temporarily turn off // change events. currCodeSelect.removeItemListener( theItemListener ); currCodeSelect.setSelectedIndex( newCurrencyIndex ); currCodeSelect.addItemListener( theItemListener ); } if ( currencyCodeDisplay != null ) { currencyCodeDisplay.setText( exchs[ newCurrencyIndex ].getCurrAbbr() ); } if ( currenceNameDisplay != null ) { currenceNameDisplay.setText( exchs[ newCurrencyIndex ].getCurrName() ); } calcAmounts(); // abbreviated and/or precise repaint(); } /** * guess at user's preferred viewing currency based on his country locale */ private void determineUsersHomeCurrency() { try { String country = Locale.getDefault().getCountry(); // upper case two-letter iso code if ( country == null || country.length() != 2 ) { currentCurrencyIndex = -1; err.println( "Warning OS failed to divulge the user's country: " + country ); } else { userCurrencyAbbr = countryToCurr.get( country ); currentCurrencyIndex = findCurrencyInfo( userCurrencyAbbr ); } } catch ( Exception e ) { currentCurrencyIndex = -1; err.println( "Warning OS failed to divulge the user's country" ); } if ( currentCurrencyIndex < 0 ) { /* user in a country for which we have no exchange rate data */ /* Show him US$ */ userCurrencyAbbr = "USD"; currentCurrencyIndex = findCurrencyInfo( userCurrencyAbbr ); } // we don't set that choice in currCodeSelect until later in buildComponents. } /** * read the Applet parameters. * * @return String of letters indicating what we should display. */ private String getAppletParameters() { /* get Applet param data */ final String geom = getParameter( "geom" ); if ( geom == null ) { err.println( "Warning: missing geom version parameter" ); } else { long websiteGeom = Long.parseLong( geom ); if ( websiteGeom != Geom.serialVersionUID ) { err.println( "Warning: macros were expanded with version " + websiteGeom + " instead of the expected " + Geom.serialVersionUID ); } } String whatToShow = getParameter( "show" ); /* should be some combination of letters cCN$A */ if ( whatToShow == null ) { whatToShow = "I";/* invalid */ } /* get amount from = 0 || whatToShow.indexOf( 'P' ) >= 0 ) { String amountValue = getParameter( "amount" ); try { if ( amountValue == null ) { throw new NumberFormatException(); } amountValue = ST.stripCommas( amountValue ); // don't use Double.parseDouble, not available in 1.1 // not locale-sensitive baseAmount = Double.valueOf( amountValue ); } catch ( NumberFormatException e ) { baseAmount = 0.00; err.println( "invalid value for amount param: " + amountValue ); whatToShow = "I"; } /* get currency from