#!/usr/bin/env perl
use warnings;

# Updated April 27, 2016.

# Calculate Social Security early-start discounts and break-even ages for
# various birth years.  See:  http://www.ssa.gov/retire2/retirechart.htm and
# related pages, such as:     http://www.ssa.gov/retire2/delayret.htm
# Checked some output from this script against the data from these webpages.

# For usage summary, run with no arguments.

    $debug = 0;  # set to call CalculateBE().

# DISCUSSION:  Every eligible person can start their ordinary SS benefits at
# any month of age between 62 and 70.  Their exact SS FRA (full retirement age,
# ~66-67) depends on their birth year.  For everyone, benefits are REDUCED
# (discounted) 5/12%/month for each month starting early between 62.0 and
# FRA-36 months, then 5/9%/month until FRA, then INCREASED 8/12%/month for each
# additional month of deferral after FRA.  (Shown in SS webpages rounded to
# nearest 0.1% at each point, but apparently full math is used with rounding of
# payments to $0.01.)
#
# This script operates following the SS model, but also adds information in
# terms of a pure discount for any starting point before age 70 (rather than a
# bonus after FRA) because that's simpler and ultimately more meaningful.
#
# Note well:  The post-FRA DISCOUNT rate is not 8/12 per month; that's the
# INCREASE (bonus) rate for deferring beyond FRA.  An INCREASE rate of 8% =
# (1.08/1.0 - 1) for one year is a DISCOUNT rate of 1 - (1/1.08) for one year.
# However it's trickier than that for multiple years.  The annual post-FRA
# discount rate must be calculated as the inverse of the overall bonus amount,
# then divided by the number of years between FRA and age 70, NOT merely as the
# inverse of the bonus rate for ONE year.
#
# For example, someone with FRA = 67y0m gains 3 * 8% = 24% by waiting until age
# 70.  The nominal single year discount is:  1 - (1 / 1.08) = 0.074075, but the
# true annual discount rate is:  (1 - (1 / 1.24)) / 3 = 0.064516.
#
# All this matters because the time to BE is 1/D (discount rate); see
# CalculateBE().  For FRA = 67, BE at FRA is 1 / 0.064516 = 15.5 years.  The
# longer from FRA to age 70 (for earlier birth years), even at a constant
# 8%/year deferral bonus for all recipients, the lower the effective annual
# discount rate, so the longer the time to BE and the greater the motivation to
# not defer.  For example with FRA = 66, D = (1 - (1 / 1.32)) / 4 = 0.060606,
# and BE = 1 / 0.060606 = 16.5 years.

# CHECK ARGUMENTS:
#
# Note:  In or after 2014 there's no point dealing with birth years before 1945
# because older people have already reached age 70.

    ($MyName = $0) =~ s,.*/,,;  # basename.

    $opt_m = 0;
    if (($#ARGV >= 0) && ($ARGV[0] eq '-m')) {$opt_m = 1; shift(@ARGV);}

      (($#ARGV != 0)
    ||  ($ARGV[0] eq '-?')
    ||  ($ARGV[0] eq '-help')
    ||  ($ARGV[0] eq '--help')
    || (($ARGV[0] <=   44) && ($ARGV[0] >=   61))
    || (($ARGV[0] <= 1944) && ($ARGV[0] >= 1961)))
    && Usage();

    $birthyear = (($ARGV[0] >= 1900) ? $ARGV[0] : ($ARGV[0] + 1900));

# PRINT SS "FULL RETIREMENT AGE" (FRA) in years,months:

    if ($birthyear <= 1959)
    {
        $FRAy = 66;
        $FRAm = (($birthyear <= 1954) ? 0 : (2 * ($birthyear - 1954)));
    }
    else {$FRAy = 67; $FRAm = 0;}

    print "Birth year = $birthyear: FRA = $FRAy years + $FRAm months.\n\n";

# INITIALIZE further:
#
# Abbreviations:
#
# - M = monthly (not yearly).
# - Y = yearly.
# - T = total (all months).
# - P = percentage (not fraction).

    $miny = 62.0;  # lowest starting age for everyone.

# See header comments about these discount/bonus rates:

    $MPdisc1  = 5/12;  # monthly % rate of discount before FRA-36 months.
    $MPdisc2  = 5/9;   # monthly % rate of discount between FRA-36 and FRA.
    $MPbonus3 = 8/12;  # monthly % rate of bonus from FRA to age 70.

    $mon_postFRA = ((70 - $FRAy) * 12) - $FRAm;  # from FRA to age 70.
    $TPmax       = 100 + ($mon_postFRA * $MPbonus3);   # max payment at age 70.
    $MPdisc3     = (1 - (100 / $TPmax)) / $mon_postFRA * 100;  # versus age 70.

    printf("SS discount/bonus rates when starting payments before age 70:\n"
         . "%.4f%% (5/12) monthly discount before FRA-36 months\n"
         . "%.4f%% (5/9)  monthly discount between FRA-36 months and FRA\n"
         . "%.4f%% (8/12) monthly bonus for deferring after FRA\n"
         . "%.4f%% equivalent monthly discount rate between this FRA and age 70\n",
         $MPdisc1, $MPdisc2, $MPbonus3, $MPdisc3);

    print "Note: The first three values are the same for everyone born 1943 or later.\n",
          "The last value depends on your birth year => FRA => number of months from FRA\n",
	  "until age 70.  While the bonus rate is fixed at 8%/year, the effective annual\n",
	  "or monthly discount rate versus age 70 depends on the time span.\n\n";

# PREPARE TO PRINT:

    print "START  TIL VS-FRA  VS-70     EFF   BREAK-EVEN\n",
          "YR MO  FRA   PAY%   PAY%   DISC%  YEARS   AGE\n";

    $format = "%2d %2d  %+3d %6.1f %6.1f %7.4f  %5.1f %5.1f\n";

# PRINT SS DISCOUNT TABLE FOR STARTING PAYMENTS BEFORE FRA:
#
# Note:  Work with integer years and months, although tedious, to avoid
# accumulated rounding errors.

    for $year ($miny .. $FRAy)
    {
        for $month (0 .. 11)
        {
            $opt_m || ($month == 0) || next;  # skip partial years unless -m.

            ($year >= $FRAy) && ($month >= $FRAm) && last;  # stop BEFORE FRA.
            $mon_preFRA = (($FRAy - $year) * 12) + ($FRAm - $month);
            $mon_pre70  = ((70    - $year) * 12) - $month;

            $TPdiscFRA = (($mon_preFRA <= 36) ?      # discount versus FRA.
                          ($mon_preFRA * $MPdisc2) :
                          ((($mon_preFRA - 36) * $MPdisc1) + (36 * $MPdisc2)));

            $TPfracFRA = 100 - $TPdiscFRA;           # fraction versus FRA.
            $TPfrac70  = $TPfracFRA / $TPmax * 100;  # fraction versus age 70.
            $TPdisc70  = 100 - $TPfrac70;

# The time to BE for a discounted early-start pension is just 1 / (annual
# discount fraction); see CalculateBE().  Although for SS, unlike some
# pensions, this time varies depending on the starting month.  And here it's
# the fraction relative to age 70, not FRA.

	    $MPeff_disc = $TPdisc70 / $mon_pre70;
            $BE_years   = 1 / ($MPeff_disc / 100 * 12);
            $BE_age     = $year + ($month / 12) + $BE_years;

            $opt_m && ($month == 0) && print "\n";

            printf($format, $year, $month, -$mon_preFRA, $TPfracFRA, $TPfrac70,
                   $MPeff_disc, $BE_years, $BE_age);

            $debug && CalculateBE($year, $month, $TPfrac70, $BE_age);
        }
    }

# PRINT SS BONUS TABLE FOR DEFERRING AFTER FRA:

    $MPeff_disc = $MPdisc3;  # a constant after FRA.
    $BE_years = 1 / ($MPeff_disc / 100 * 12);

    for $year ($FRAy .. 70)
    {
        for $month (0 .. 11)
        {
            $opt_m || ($month == 0) || next;  # skip partial years unless -m.

            ($year <= $FRAy) && ($month < $FRAm) && next;  # start at FRA.
            ($year == 70)    && ($month >=    1) && last;  # stop after 70,0.

# TBD all this math is still wrong, confusing bonus/discount handling, don't
# get consistent results from CalculateBE():

            $mon_postFRA = (($year - $FRAy) * 12) + ($month - $FRAm);
            $TPbonus     = 100 + ($mon_postFRA * $MPbonus3);
            $TPfrac70    = $TPbonus / $TPmax * 100;  # fractional pay vs age 70.
            $BE_age      = $year + ($month / 12) + $BE_years;

            $opt_m && ($month == 0) && print "\n";

            printf($format, $year, $month, $mon_postFRA, $TPbonus, $TPfrac70,
                   $MPeff_disc, $BE_years, $BE_age);

            $debug && CalculateBE($year, $month, $TPfrac70, $BE_age);
        }
    }
    exit(0);

#==============================================================================
# U S A G E
#
# Given global $MyName, print usage message and exit.

sub Usage
{
    print <<UNQUOTED_HERE_DOC
Usage: $MyName [-m] <birth_year>  # 45..60|1945-1960; use 60 for 1960 or later.

-m causes more-verbose monthly rather than just yearly line items

Calculates Social Security early-start discounts and break-even ages for
various birth years.

Note, as Tom von Alten said:  "The SS program design is to provide for an early
start for those who need it, and to allow deferral for those who can and want
it to provide better 'longevity risk' insurance.  The design 'break-even' is
based on actuarial estimates [although possibly outdated using shorter
life-spans than apply today]."
UNQUOTED_HERE_DOC
;

    exit(0);
}

#==============================================================================
# C A L C U L A T E   B E
#
# For debugging:  Given:
#
# - starting year and month
# - total percentage fraction of full payment at age 70 for starting at that
#   time (trust the caller on this)
# - caller's idea of BE age
# - global $MP*
#
# Brute force calculate and print the break-even age (fractional years of age)
# for starting SS payments early versus age 70, and compare with the caller's
# value.  This is when total payments for starting late exceed the total for
# starting early.
#
# Note:  Due to rounding errors, this matchup is not precise.
#
# From my http://silgro.com/PensionEarly.htm webpage:
#
# "BE (break even) is the point in time when receiving less monthly or annual
# income (multiplying F [full payment at true FRA] by 1 - (X * D)) for a longer
# time (X + Y) [X = years before FRA, Y = years after] equals receiving more
# monthly or annual income (FRA full pension amount) for less time (Y):
#
#   F * (1 - (X * D)) * (X + Y) = F * Y
#
# ...this equation reduces to:
#
#   (1 / D) = X + Y"
#
# This function confirms the math for SS where D somewhat depends on starting
# age and FRA.

sub CalculateBE
{
    my($starty, $startm, $TPfrac, $BE_age) = @_;

    my($year) = $starty;
    my($early) = my($late) = 0;  # accumulated payments, as if full = $1/month.

LOOP:
    while(1)
    {
        for $month (0 .. 11)
        {
            ($year == $starty) && ($month < $startm) && next;  # too early.

            $early += $TPfrac / 100;  # get fraction monthly.
            $late  += ($year >= 70);  # get full amount (1.00 here) monthly.

          # printf("xxx %2d,%2d: %6.3f %6.3f\n", $year, $month, $early, $late);

            if ($early < $late)
            {
                my($BE_calc) = $year + ($month / 12);  # actually a bit over.

                printf("debug: BE = %.3f, diff %.4f; early, late = %.3f, %d\n",
                       $BE_calc, $BE_calc - $BE_age, $early, $late);
              # print "\n"; # xxx.
                return;
            }
        }
        if (++$year >= 100)  # shouldn't happen until end, $starty = 70:
        {
            printf("debug: CalculateBE() did not terminate; BE = %.1f, early, late = %.3f, %d\n",
                   $year, $early, $late);
            return;
        }
    }

} # CalculateBE()
