diff --git a/_posts/2022-01-11-substantial-presence-test/index.mdx b/_posts/2022-01-11-substantial-presence-test/index.mdx
index e61cd8b..9eb31c9 100644
--- a/_posts/2022-01-11-substantial-presence-test/index.mdx
+++ b/_posts/2022-01-11-substantial-presence-test/index.mdx
@@ -4,8 +4,23 @@ date: 2022-01-11
status: draft
---
-The substantial presence test can be a bit confusing. Here's a simple calculator to help you determine whether you pass.
+The IRS uses the [Substantial Presence
+Test](https://www.irs.gov/individuals/international-taxpayers/substantial-presence-test) to determine whether
+a person is a resident for tax purposes. As of the time this calculator was built, the requirements for the
+test are that the person must be physically present in the United States on at lesat:
+
+1. 31 days during the current year, and
+2. 183 days during the 3-year period that includes the current year and the 2 years immediately before that,
+ counting:
+ - All the days you were present in the current year, and
+ - 1/3 of the days you were present in the first year before the current year, and
+ - 1/6 of the days you were present in the second year before the current year.
+
+This can be a bit confusing to calculate, so I've created a tool to help. Your changes will be automatically
+saved to your browser so you can come back later on the same device and get an updated answer if necessary.
+
+
diff --git a/components/MdxRenderer.tsx b/components/MdxRenderer.tsx
index 12b61e2..42ea3d9 100644
--- a/components/MdxRenderer.tsx
+++ b/components/MdxRenderer.tsx
@@ -21,7 +21,9 @@ const shortcodes = {
h4: createHeadingComponent('h4'),
h5: createHeadingComponent('h5'),
h6: createHeadingComponent('h6'),
- SubstantialPresenceTest, // TODO: Should be able to import from just the article that uses this
+
+ // TODO: Should be able to import these from just the articles that uses them
+ SubstantialPresenceTest,
RocketLeague,
};
diff --git a/components/SubstantialPresenceTest.tsx b/components/SubstantialPresenceTest.tsx
index 1df65dd..71f6238 100644
--- a/components/SubstantialPresenceTest.tsx
+++ b/components/SubstantialPresenceTest.tsx
@@ -1,313 +1,394 @@
-import { Fragment, useEffect, useReducer, useState } from 'react';
+import { useAtom, useAtomValue } from 'jotai';
+import { atomWithStorage } from 'jotai/utils';
+import { FC } from 'react';
+import { addDays, differenceInDays } from 'date-fns';
-const NUM_YEARS_TO_INCLUDE = 10;
const CURRENT_YEAR = new Date().getFullYear();
const TOTAL_REQUIREMENT = 183;
-const YEAR_REQUIREMENTS = [
- {
- yearOffset: 0,
- minimum: 31,
- multiplier: 1,
- },
- {
- yearOffset: -1,
- multiplier: 1 / 3,
- },
- {
- yearOffset: -2,
- multiplier: 1 / 6,
- },
-];
-
-function dateToValue(date: Date) {
- return date.toISOString().split('T')[0];
+const CURRENT_YEAR_REQUIREMENT = 31;
+const YEAR_MULTIPLIERS = [1, 1 / 3, 1 / 6];
+const NUM_YEARS_TO_INCLUDE = YEAR_MULTIPLIERS.length * 4;
+
+interface Range {
+ start: number;
+ end: number;
}
-function addDays(date: Date, days: number) {
- const newDate = new Date(date);
- newDate.setDate(newDate.getDate() + days);
- return newDate;
+interface YearDetails {
+ present: boolean;
+ ranges: Range[];
}
-type DayCounterRangeProps = {
- year: number;
- onChange?: (range: DayCounterRange) => void;
-};
+const yearAtom = atomWithStorage(
+ 'substantialPresenceTest:selectedYear',
+ CURRENT_YEAR
+);
+const yearsDetailsAtom = atomWithStorage>(
+ 'substantialPresenceTest:yearsDetails',
+ {}
+);
-const DayCounterRange = function ({ year, onChange }: DayCounterRangeProps) {
- const min = `${year}-01-01`;
- const max = year !== CURRENT_YEAR ? `${year}-12-31` : dateToValue(new Date());
+function getAllValidDays(
+ year: number,
+ yearsDetails: Record
+) {
+ return YEAR_MULTIPLIERS.map((multiplier, i) => {
+ const offsetYear = year - i;
+ return getValidDays(multiplier, yearsDetails[offsetYear]);
+ }).reduce(
+ (acc, curr) => ({
+ totalDays: acc.totalDays + curr.validDays,
+ validDays: acc.validDays + curr.validDays,
+ lastDay: Math.max(curr.lastDay ?? 0, acc.lastDay ?? 0),
+ }),
+ { totalDays: 0, validDays: 0, lastDay: 0 }
+ );
+}
- const [start, setStart] = useState(new Date(min));
- const [end, setEnd] = useState(new Date(max));
+function getValidDays(multiplier: number, details?: YearDetails) {
+ const totalDays = details?.present
+ ? details.ranges.reduce(
+ (total, range) => total + differenceInDays(range.end, range.start) + 1,
+ 0
+ )
+ : 0;
+ const validDays = Math.floor(totalDays * multiplier);
+ return { totalDays, validDays, lastDay: details?.ranges.at(-1)?.end };
+}
- function computeChange() {
- if (onChange) {
- const timeDiff = end.getTime() - start.getTime();
- const daysDiff = timeDiff / (1000 * 3600 * 24);
- onChange({ count: daysDiff, start, end }); // TODO: Should we include the last day?
- }
- }
+const SubstantialPresenceTest = () => {
+ const year = useAtomValue(yearAtom);
+ const yearsDetails = useAtomValue(yearsDetailsAtom);
- useEffect(() => {
- computeChange();
- }, [start, end]);
+ const { validDays, lastDay } = getAllValidDays(year, yearsDetails);
+ const { validDays: currentYearValidDays } = getValidDays(
+ YEAR_MULTIPLIERS[0],
+ yearsDetails[year]
+ );
- function handleChange(setter: (date: Date) => void, value: Date) {
- setter(value);
- }
+ const dayOfPassing =
+ validDays >= TOTAL_REQUIREMENT
+ ? currentYearValidDays > CURRENT_YEAR_REQUIREMENT
+ ? new Date(lastDay!)
+ : addDays(lastDay!, CURRENT_YEAR_REQUIREMENT - currentYearValidDays)
+ : addDays(lastDay!, TOTAL_REQUIREMENT - validDays);
return (
-
- {' '}
-
-
+
+