/*
* [Holidays.java]
*
* Summary: Calculate when various holidays occur.
*
* Copyright: (c) 1999-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 1999-09-09 to show off the Holidays class
* 1.1 1999-09-10 add St Patrick's Day and Commonwealth Day
* 1.2 1999-09-17 reorg so each holiday calculator is a separate class
* add equinox and solstice calculators.
* 1.3 1999-10-24 add Robbie Burns Day
* 1.4 2000-05-08 more accurate Victoria Day calculation.
* 1.5 2001-01-14 configurable default year
* 1.6 2001-04-07 add inauguration day
* 1.7 2002-01-08 change default date to 2003
* fix bug in Victoria Day calc.
* 1.8 2002-03-16 make fromYear and toYear protected to make it easier to override
* in isHoliday.java.
* 1.9 2002-03-18 improve Javadoc
* improve constructor for IsHoliday class
* 2.0 2004-04-24 add Earth Day
* 2.1 2005-02-12 change default year to 2005
* 2.2 2005-06-18 consistent bat files.
* 2.3 2005-06-18 about box
* 2.4 2005-07-27 added Parents' Day
* added extensive Javadoc
* 2.5 2005-12-01 added Human Rights Day
* 2.6 2006-03-05 reformat with IntelliJ, add Javadoc
* 2.7 2006-03-05
* 2.8 2006-03-05
* 2.9 2006-03-05
* 3.0 2006-03-05
* 3.1 2007-05-22 add PAD and icon, more IntelliJ inspect tidying.
* 3.2 2007-10-10 additional notes.
* 3.3 2007-10-13 UK vs US Christmas, calculate when get day off.
* 3.4 2007-12-19 display year to ensure have correct one.
* 3.5 2008-01-25 add Australia Day
* 3.6 2008-02-02 add ANZAC day, Australian Queen's Birthday
* 3.7 2008-02-07 add Mardi Gras
* 3.8 2008-03-20 convert to JDK 1.5, sort holidays, tidy word wraps, use of rsquo
* 3.9 2008-06-21 add Aboriginal Day
* 4.0 2008-10-19 add Daylight Saving Start and Daylight Saving End Day
* 4.1 2008-11-01 add St. Andrews day
* 4.2 2008-12-03 add World AIDS day
* 4.3 2009-09-15 add Change Your Password Day
* 4.4 2009-11-23 add Black Friday
* 4.5 2010-03-30 add Earth Hour
* 4.6 2010-10-23 add Creationism Day
* 4.7 2011-02-16 add ExportHolidaysAsCSV
* 4.8 2013-01-29 add Darwin Day
* 4.9 2013-02-10 add BC Family Day
* 5.0 2014-03-17 add No Bag day
* 5.1 2014-04-16 add Sweater Day
* 5.2 2014-10-05 add Orange shirt day and Sorry Day
*/
package com.mindprod.holidays;
import com.mindprod.common18.BigDate;
import com.mindprod.common18.Build;
import com.mindprod.common18.CMPAboutBox;
import com.mindprod.common18.Common18;
import com.mindprod.common18.FontFactory;
import com.mindprod.common18.HybridJ;
import com.mindprod.common18.JEButton;
import com.mindprod.common18.Misc;
import com.mindprod.common18.VersionCheck;
import com.mindprod.fastcat.FastCat;
import javax.swing.JApplet;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTextArea;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.Color;
import java.awt.Container;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import static java.lang.System.*;
// todo: display as printable calendar for month
/**
* Calculate when various holidays occur.
*
* HolidayCalculator.java CMP Holiday Calculator HolidayCalculator Enter a year, and it displays the dates of all the
* holidays in that year. If a holiday was not observed in a given year, it will not show. The classes it uses for
* calculating holidays can be easily cannibalised for your own applications. This calculator does not demonstrate the
* use of the IsHoliday class to rapidly determine if a given day is a holiday.
*
* Major classes to look at: HolidayCalculator HolInfo IsHoliday Maker The rest are classes about
* individual holidays. In a future version the individual holidays will be put into a separate
* package to make it easier to find the generic stuff.
*
*
* @author Roedy Green, Canadian Mind Products
* @version 5.2 2014-10-05 add Orange shirt day and Sorry Day
* @noinspection FieldCanBeLocal
* @see com.mindprod.holidays.ExportHolidaysToCSV
* @since 1999-09-09
*/
public final class Holidays extends JApplet
{
private static final Font FONT_FOR_LABELS = FontFactory.build( "Dialog", Font.BOLD, 15 );
/**
* height of Applet box in pixels. Does not include surrounding frame.
*/
private static final int APPLET_HEIGHT = 480;
/**
* Width of Applet box in pixels.
*/
private static final int APPLET_WIDTH = 720;
/**
* Default year to display a list of holidays for.
*/
private static final int defaultYear = Misc.thisYear();
private static final int FIRST_COPYRIGHT_YEAR = 1999;
/**
* undisplayed copyright notice
*
* @noinspection UnusedDeclaration
*/
private static final String EMBEDDED_COPYRIGHT =
"Copyright: (c) 1999-2017 Roedy Green, Canadian Mind Products, http://mindprod.com";
private static final String RELEASE_DATE = "2014-10-05";
private static final String TITLE_STRING = "CMP Holiday Calculator";
private static final String VERSION_STRING = "5.2";
/**
* the colour white.
*/
private static final Color BACKGROUND_FOR_APPLET = Color.WHITE;
/**
* the colour black.
*/
private static final Color FOREGROUND_FOR_DISPLAY = Color.BLACK;
/**
* the colour dark green.
*/
private static final Color FOREGROUND_FOR_INSTRUCTIONS = new Color( 0x008000 );
private static final Color FOREGROUND_FOR_LABEL = new Color( 0x0000b0 );
/**
* for titles
*/
private static final Color FOREGROUND_FOR_TITLE = new Color( 0xdc143c );
/**
* for for titles and About buttons
*/
private static final Font FONT_FOR_TITLE = FontFactory.build( "Dialog", Font.BOLD, 16 );
/**
* for for titles and About buttons
*/
private static final Font FONT_FOR_TITLE2 = FontFactory.build( "Dialog", Font.PLAIN, 14 );
/**
* data entry text field colour
*/
private static final Color FOREGROUND_FOR_ENTER = Color.BLACK;
/**
* background where for data entry where user enters a value.
*/
private static final Color BACKGROUND_FOR_EDITABLE = Color.WHITE;
/**
* formats output of saleAmount spinner.
*/
private JSpinner.NumberEditor yearNumberEditor;
/**
* dollar amount of sale with a spinner to increment/decrement by pennies
* setValue ( double ); (Double) getValue.
*/
private JSpinner yearSpinner;
/**
* A delegate object that implements the HolInfo abstract class. That lets us execute code for a number of different
* holidays given only the name of the class that computes them. We dynamically load the HolInfo classes with
* Class.forName.
*/
private HolInfo[] holidayDelegate;
/**
* tracks value of the sale amount spinner.
*/
private SpinnerNumberModel yearSpinnerModel;
/**
* ACTUAL if you want the actual date of the holiday. SHIFTED if you want the date taken off work, usually the
* nearest weekday.
*/
private JComboBox shift;
/**
* choice of terse or verbose calendar.
*/
private JComboBox terse;
/**
* about box
*/
private JEButton about;
/**
* prompt on how to use the Applet.
*/
private JLabel instructions;
/**
* Label for year field
*/
private JLabel theYearLabel;
/**
* Applet title
*/
private JLabel title;
/**
* title, second line
*/
private JLabel title2;
/**
* scroll results
*/
private JScrollPane scroller;
/**
* The region where we display the list of holidays
*/
private JTextArea display;
/**
* Prepare the year in a form suitable for display with BC instead of -ve sign.
*
* @param year (-ve for BCE)
*
* @return String display of year BCE for negative years
*/
private static String yearToDisplay( int year )
{
if ( year < 0 )
{
return Integer.toString( -year ) + " BCE";
}
else if ( year == 0 )
{
return "";
}
else
{
return Integer.toString( year );
}
}
/**
* allocate the components
*/
private void buildComponents()
{
title = new JLabel( TITLE_STRING + " " + VERSION_STRING );
title.setFont( FONT_FOR_TITLE );
title.setForeground( FOREGROUND_FOR_TITLE );
title.setFont( FONT_FOR_TITLE );
title.setForeground( FOREGROUND_FOR_TITLE );
title2 = new JLabel(
"released:" +
RELEASE_DATE +
" build:" +
Build.BUILD_NUMBER
);
title2.setFont( FONT_FOR_TITLE2 );
title2.setForeground( FOREGROUND_FOR_TITLE );
about = new JEButton( "about" );
about.addActionListener( new ActionListener()
{
public void actionPerformed( ActionEvent e )
{
// open aboutbox frame
new CMPAboutBox( TITLE_STRING,
VERSION_STRING,
"Calculate when holidays occur",
"in any given year.",
"freeware",
RELEASE_DATE,
FIRST_COPYRIGHT_YEAR,
"Roedy Green",
"HOLIDAYS",
"1.8" );
}
} );
shift = new JComboBox<>();
shift.addItem( "actual" );
shift.addItem( "nearest weekday" );
shift.setSelectedIndex( 0 );
terse = new JComboBox<>();
terse.addItem( "terse" );
terse.addItem( "verbose" );
terse.setSelectedIndex( 0 );
yearSpinner = new JSpinner();
yearSpinner.setFont( FONT_FOR_LABELS );
yearSpinner.setForeground( FOREGROUND_FOR_ENTER );
yearSpinner.setBackground( BACKGROUND_FOR_EDITABLE );
yearSpinner.setValue( defaultYear );
yearSpinnerModel = new SpinnerNumberModel( /* initial value */ defaultYear,
/* min */ 1,
/* max */ 9999,
/* step */ 1 );
yearNumberEditor = new JSpinner.NumberEditor( yearSpinner, "####" );
yearSpinner.setModel( yearSpinnerModel );
yearSpinner.setEditor( yearNumberEditor );
theYearLabel = new JLabel( "year", JLabel.CENTER );
theYearLabel.setFont( FontFactory.build( "Dialog", Font.BOLD, 15 ) );
theYearLabel.setForeground( FOREGROUND_FOR_LABEL );
display = new JTextArea( "", 0, 0 );
display.setEditable( false );
display.setMargin( new Insets( 4, 4, 4, 4 ) );
// Serif has best chance of supporting Unicode.
// So far only NT+Internet Explorer works
display.setFont( FontFactory.build( "Serif", Font.PLAIN, 15 ) );
display.setForeground( FOREGROUND_FOR_DISPLAY );
scroller =
new JScrollPane( display,
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER );
instructions =
new JLabel(
"Select options, enter year (negative for BCE) and hit enter.",
JLabel.CENTER );
instructions.setForeground( FOREGROUND_FOR_INSTRUCTIONS );
}
/**
* prepare list of all holidays in given year
*
* @param year Year for which you want a calendar prepared.
* @param shifted ACTUAL = false if you want the actual date of the holiday. SHIFTED = true if you want the date
* taken off work, usually the nearest weekday.
* @param verbose true if you want want the full story about each holiday, false if just the summary.
*
* @return list of holidays and information about them occurring in that year -- not a calendar per se.
*/
private String displayHolidays( int year, boolean shifted, boolean verbose )
{
final FastCat sb = new FastCat( holidayDelegate.length * ( verbose ? 25 : 11 ) + 5 );
sb.append( "Holidays in year " );
sb.append( year );
sb.append( "." );
if ( shifted )
{
sb.append( " nearest weekday" );
}
sb.append( "\n\n" );
for ( HolInfo h : holidayDelegate )
{
if ( h == null )
{
continue;
}
BigDate d = new BigDate( h.when( year, shifted ) );
if ( d.getOrdinal() == BigDate.NULL_ORDINAL )
{
continue;
}
sb.append( d.toString() ); // yyyyy-mm-dd
sb.append( " " );
sb.append( BigDate.dayName( d.getDayOfWeek() ) );
sb.append( " " );
sb.append( BigDate.monthName( d.getMM() ) );
sb.append( " " );
sb.append( d.getDD() );
sb.append( " is " );
sb.append( shifted ? " nearest weekday to " : "" );
sb.append( h.getName() );
sb.append( ".\n" );
if ( verbose )
{
sb.append( h.getRule() );
sb.append( "\n" );
int yearFirstObserved = h.getFirstYear( HolInfo.OBSERVED );
int yearFirstProclaimed = h.getFirstYear( HolInfo.PROCLAIMED );
sb.append( "first observed " );
sb.append( yearToDisplay( yearFirstObserved ) );
sb.append( ". " );
if ( yearFirstObserved != yearFirstProclaimed )
{
sb.append( "first proclaimed " );
sb.append( yearToDisplay( yearFirstProclaimed ) );
sb.append( "." );
}
sb.append( "\n" );
String authority = h.getAuthority();
if ( authority.length() > 0 )
{
sb.append( "authority: " );
sb.append( authority );
sb.append( "\n" );
}
sb.append( "\n" );
}
} // end for
return sb.toString();
}
/**
* get list of holidays from properties file: com.mindprod.holidays.Holiday.properties which lives in the jar. class
* names are case-sensitive. file looks like this:
Christmas=yes
GroundhogDay=no
...
where
* yes=include no=ignore
*/
private void findHolidays()
{
String[][] result = null;
try
{
// look in jar, and on classpath for
// com.mindprod.holidays.Holiday.properties
final InputStream fis = Holidays.class.getResourceAsStream( "Holiday.properties" );
result = Misc.loadProperties( fis );
// we now key=value pairs
}
catch ( IOException oops )
{
out.println( oops + " Problem accessing Holiday.properties file." );
System.exit( 1 );
}
// in pairs className=yes/no
int length = result[ 0 ].length;
int j = 0;
// keep just the holidays marked yes.
// in the holidayDelegate array.
holidayDelegate = new HolInfo[ length ];
for ( int i = 0; i < length; i++ )
{
try
{
if ( result[ 1 ][ i ].equalsIgnoreCase( "yes" ) )
{
holidayDelegate[ j++ ] = ( HolInfo ) ( Class
.forName( "com.mindprod.holidays."
+ result[ 0 ][ i ] )
.newInstance() );
}
}
catch ( Exception oops )
{
out.println( oops
+ " Bug in Holiday.properties or class file for "
+ result[ 0 ][ i ] );
System.exit( 1 );
}
} // end for
}
/**
* layout components in a GridBag
*
* @param contentPane where to place the components.
*/
private void layoutComponents( Container contentPane )
{
// Layout looks like this:
// 0 --0------1-------- 2
// 0 -title1--title2-----about
// 1 shift terse 2001
// 2 year
// 3 -------results---------
// 4 -------instructions----
// 0 --0-------1 ----------2
// x y w h wtx wty anchor fill T L B R padx pady
contentPane.add( title,
new GridBagConstraints( 0,
0,
1,
1,
30.0,
0.0,
GridBagConstraints.CENTER,
GridBagConstraints.HORIZONTAL,
new Insets( 10, 10, 5, 5 ),
0,
0 )
);
contentPane.add( title2,
new GridBagConstraints( 1,
0,
1,
1,
30.0,
0.0,
GridBagConstraints.CENTER,
GridBagConstraints.HORIZONTAL,
new Insets( 10, 5, 5, 5 ),
0,
0 )
);
// x y w h wtx wty anchor fill T L B R padx pady
contentPane.add( about,
new GridBagConstraints( 2,
0,
1,
1,
20.0,
0.0,
GridBagConstraints.EAST,
GridBagConstraints.NONE,
new Insets( 10, 5, 10, 10 ),
10,
2 )
);
// x y w h wtx wty anchor fill T L B R padx pady
contentPane.add( shift,
new GridBagConstraints( 0,
1,
1,
1,
0.0,
0.0,
GridBagConstraints.EAST,
GridBagConstraints.HORIZONTAL,
new Insets( 0, 10, 0, 5 ),
150,
0 )
);
// x y w h wtx wty anchor fill T L B R padx pady
contentPane.add( terse,
new GridBagConstraints( 1,
1,
1,
1,
0.0,
0.0,
GridBagConstraints.EAST,
GridBagConstraints.HORIZONTAL,
new Insets( 0, 5, 0, 5 ),
150,
0 )
);
// x y w h wtx wty anchor fill T L B R padx pady
contentPane.add( yearSpinner,
new GridBagConstraints( 2,
1,
1,
1,
0.0,
0.0,
GridBagConstraints.EAST,
GridBagConstraints.HORIZONTAL,
new Insets( 0, 5, 0, 10 ),
0,
0 )
);
// x y w h wtx wty anchor fill T L B R padx pady
contentPane.add( theYearLabel,
new GridBagConstraints( 2,
2,
1,
1,
0.0,
0.0,
GridBagConstraints.EAST,
GridBagConstraints.NONE,
new Insets( 0, 5, 10, 10 ),
0,
0 )
);
// x y w h wtx wty anchor fill T L B R padx pady
contentPane.add( scroller,
new GridBagConstraints( 0,
3,
3,
1,
0.0,
100.0,
GridBagConstraints.CENTER,
GridBagConstraints.BOTH,
new Insets( 0, 10, 10, 10 ),
0,
0 )
);
// x y w h wtx wty anchor fill T L B R padx pady
contentPane.add( instructions,
new GridBagConstraints( 0,
4,
3,
1,
0.0,
0.0,
GridBagConstraints.CENTER,
GridBagConstraints.NONE,
new Insets( 0, 10, 10, 10 ),
0,
0 )
);
}
/**
* Refresh screen after shift or number change.
*/
private void refresh()
{
int year = defaultYear;
try
{
year = yearSpinnerModel.getNumber().intValue();
}
catch ( NumberFormatException e )
{
// leave year at default
}
// avoid illegal year 0
if ( year == 0 )
{
year = defaultYear;
}
final boolean shifted = shift.getSelectedIndex() != 0;
final boolean verbose = terse.getSelectedIndex() != 0;
// sort holidays in order.
Arrays.sort( holidayDelegate, new HolidaySort( year, shifted ) );
// display holidays in date order
display.setText( displayHolidays( year, shifted, verbose ) );
// wind back to the top to read, setValue has no effect
// scroller.getVerticalScrollBar().setValue( 0 );
display.setCaretPosition( 0 );
}
/**
* Allow this Applet to run as as application as well.
*
* @param args command line arguments ignored.
*/
public static void main( String args[] )
{
HybridJ.fireup( new Holidays(),
TITLE_STRING + " " + VERSION_STRING,
APPLET_WIDTH,
APPLET_HEIGHT );
} // end main
/**
* Called by the browser or Applet viewer to inform
* this Applet that it is being reclaimed and that it should destroy
* any resources that it has allocated.
*/
public void destroy()
{
about = null;
display = null;
holidayDelegate = null;
instructions = null;
scroller = null;
shift = null;
terse = null;
yearSpinner = null;
theYearLabel = null;
title = null;
title2 = null;
}
/**
* Called by the browser or Applet viewer to inform
* this Applet that it has been loaded into the system.
*/
@Override
public void init()
{
if ( !VersionCheck.isJavaVersionOK( 1, 8, 0, this ) )
{
return;
}
Common18.setLaf();
final Container contentPane = this.getContentPane();
contentPane.setBackground( BACKGROUND_FOR_APPLET );
contentPane.setLayout( new GridBagLayout() );
buildComponents();
findHolidays();
refresh();
layoutComponents( contentPane );
// hook up listeners
TheListener theListener = new TheListener();
yearSpinnerModel.addChangeListener( theListener );
shift.addItemListener( theListener );
terse.addItemListener( theListener );
this.validate();
this.setVisible( true );
} // end init
/**
* Inner class to act as listener to respond to all end-user actions.
*/
private class TheListener implements ItemListener, ChangeListener
{
/**
* Notice any change to shift/terse choice.
*
* @param event details of just what the user clicked.
*/
public void itemStateChanged( ItemEvent event )
{
final Object object = event.getSource();
if ( object == shift || object == terse )
{
refresh();
} // end if
} // end itemStateChanged
/**
* spinner changed value
*
* @param e event from spinner
*/
public void stateChanged( ChangeEvent e )
{
final Object object = e.getSource();
if ( object == yearSpinnerModel )
{
refresh();
}
} // end actionPerformed
} // end TheListener
}