Skip to content

Commit 9f3d863

Browse files
authored
Merge pull request #2904 from martinholmer/actc-ctc-change
Simplify `ACTC_c` logic
2 parents ea665e3 + 4287339 commit 9f3d863

9 files changed

Lines changed: 77 additions & 28 deletions

File tree

taxcalc.egg-info/PKG-INFO

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Metadata-Version: 2.4
22
Name: taxcalc
3-
Version: 4.6.0
3+
Version: 4.6.1
44
Summary: Tax-Calculator
55
Home-page: https://github.com/PSLmodels/Tax-Calculator
66
Download-URL: https://github.com/PSLmodels/Tax-Calculator
@@ -57,3 +57,6 @@ explains the workflow involved in contributing model enhancements.
5757

5858
Complete documentation is available
5959
[here](https://PSLmodels.github.io/Tax-Calculator/).
60+
This documentation includes
61+
[examples](https://taxcalc.pslmodels.org/usage/tcja_after_2025.html)
62+
of how to analyze different ways of extending TCJA policy beyond 2025.

taxcalc/calcfunctions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3190,7 +3190,7 @@ def NonrefundableCredits(c05800, e07240, e07260, e07300, e07400,
31903190
@iterate_jit(nopython=True)
31913191
def AdditionalCTC(codtc_limited, ACTC_c, n24, earned, ACTC_Income_thd,
31923192
ACTC_rt, nu06, ACTC_rt_bonus_under6family, ACTC_ChildNum,
3193-
CTC_is_refundable, CTC_include17,
3193+
CTC_is_refundable, CTC_include17, CTC_c,
31943194
age_head, age_spouse, MARS, nu18,
31953195
ptax_was, c03260, e09800, c59660, e11200,
31963196
c11070):
@@ -3251,7 +3251,7 @@ def AdditionalCTC(codtc_limited, ACTC_c, n24, earned, ACTC_Income_thd,
32513251
childnum = n24 + max(0, nu18 - tu18 - su18 - n24)
32523252
else:
32533253
childnum = n24
3254-
line4 = ACTC_c * childnum
3254+
line4 = min(ACTC_c, CTC_c) * childnum
32553255
c11070 = 0. # line15
32563256
if line3 > 0. and line4 > 0.:
32573257
line5 = min(line3, line4)

taxcalc/policy_current_law.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21819,7 +21819,7 @@
2181921819
"validators": {
2182021820
"range": {
2182121821
"min": 0,
21822-
"max": "CTC_c"
21822+
"max": 9e+99
2182321823
}
2182421824
},
2182521825
"compatible_data": {

taxcalc/reforms/ext.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@
3434
"CTC_c": {"2026": 2000.00},
3535
"ACTC_c": {"2026": 1776.67},
3636
"ACTC_c-indexed": {"2026": true},
37-
"ACTC_c": {"2032": 2000.00},
38-
"ACTC_c-indexed": {"2032": false},
3937
"ODC_c": {"2026": 500.00},
4038
"CTC_ps": {"2026": [200000.0, 400000.0, 200000.0, 200000.0, 400000.0]},
4139
"ACTC_Income_thd": {"2026": 2500.00},

taxcalc/taxcalcio.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from taxcalc.growdiff import GrowDiff
1919
from taxcalc.growfactors import GrowFactors
2020
from taxcalc.calculator import Calculator
21-
from taxcalc.utils import (delete_file, write_graph_file,
21+
from taxcalc.utils import (json_to_dict, delete_file, write_graph_file,
2222
add_quantile_table_row_variable,
2323
unweighted_sum, weighted_sum)
2424

@@ -133,7 +133,17 @@ def __init__(self, input_data, tax_year, baseline, reform, assump,
133133
msg = f'{fname} does not end in .json'
134134
self.errmsg += f'ERROR: BASELINE file name {msg}\n'
135135
# check existence of BASELINE file
136-
if not os.path.isfile(bas):
136+
if os.path.isfile(bas):
137+
# check validity of JSON text
138+
with open(bas, 'r', encoding='utf-8') as jfile:
139+
json_text = jfile.read()
140+
try:
141+
_ = json_to_dict(json_text)
142+
except ValueError as valerr: # pragma: no cover
143+
msg = f'{bas} contains invalid JSON'
144+
self.errmsg += f'ERROR: BASELINE file {msg}\n'
145+
self.errmsg += f'{valerr}'
146+
else:
137147
msg = f'{bas} could not be found'
138148
self.errmsg += f'ERROR: BASELINE file {msg}\n'
139149
# add fname to list of basnames used in output file names
@@ -167,7 +177,17 @@ def __init__(self, input_data, tax_year, baseline, reform, assump,
167177
msg = f'{fname} does not end in .json'
168178
self.errmsg += f'ERROR: REFORM file name {msg}\n'
169179
# check existence of REFORM file
170-
if not os.path.isfile(rfm):
180+
if os.path.isfile(rfm):
181+
# check validity of JSON text
182+
with open(rfm, 'r', encoding='utf-8') as jfile:
183+
json_text = jfile.read()
184+
try:
185+
_ = json_to_dict(json_text)
186+
except ValueError as valerr: # pragma: no cover
187+
msg = f'{rfm} contains invalid JSON'
188+
self.errmsg += f'ERROR: REFORM file {msg}\n'
189+
self.errmsg += f'{valerr}'
190+
else:
171191
msg = f'{rfm} could not be found'
172192
self.errmsg += f'ERROR: REFORM file {msg}\n'
173193
# add fname to list of refnames used in output file names

taxcalc/tests/test_policy.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -811,19 +811,6 @@ def test_get_index_rate():
811811
assert pol.wage_growth_rates() == pol._wage_growth_rates
812812

813813

814-
def test_reform_with_bad_ctc_levels():
815-
"""
816-
Implement a reform with _ACTC > _CTC_c values.
817-
"""
818-
pol = Policy()
819-
child_credit_reform = {
820-
'CTC_c': {2020: 2200},
821-
'ACTC_c': {2020: 2500}
822-
}
823-
with pytest.raises(pt.ValidationError):
824-
pol.implement_reform(child_credit_reform)
825-
826-
827814
def test_reform_with_removed_parameter(monkeypatch):
828815
"""
829816
Try to use removed parameter in a reform.

taxcalc/tests/test_reforms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,4 +386,4 @@ def test_ext_reform(tests_path):
386386
iitax_ext = calc_ext.array('iitax')
387387
rdiff = iitax_ext - iitax_end
388388
weighted_sum_rdiff = (rdiff * calc_end.array('s006')).sum() * 1.0e-9
389-
assert np.allclose([weighted_sum_rdiff], [-205.769], rtol=0.0, atol=0.01)
389+
assert np.allclose([weighted_sum_rdiff], [-214.393], rtol=0.0, atol=0.01)

taxcalc/tests/test_taxcalcio.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,24 @@ def fixture_errorreformfile():
153153
pass # sometimes we can't remove a generated temporary file
154154

155155

156+
@pytest.fixture(scope='session', name='ereformfile')
157+
def fixture_ereformfile():
158+
"""
159+
Temporary reform file with .json extension.
160+
"""
161+
contents = '{"II_em": {"2022": 1000},}'
162+
with tempfile.NamedTemporaryFile(
163+
suffix='.json', mode='a', delete=False
164+
) as rfile:
165+
rfile.write(contents)
166+
yield rfile
167+
if os.path.isfile(rfile.name):
168+
try:
169+
os.remove(rfile.name)
170+
except OSError:
171+
pass # sometimes we can't remove a generated temporary file
172+
173+
156174
@pytest.fixture(scope='session', name='errorassumpfile')
157175
def fixture_errorassumpfile():
158176
"""
@@ -249,6 +267,7 @@ def fixture_assumpfile2():
249267
'no-dot-json-filename'),
250268
([], [], [], [],),
251269
('no-exist.csv', 'no-exist.json', 'no-exist.json', 'no-exist.json'),
270+
('cps.csv', 'ereformfile', 'ereformfile', 'no-exist.json'),
252271
])
253272
def test_ctor_errors(input_data, baseline, reform, assump):
254273
"""

taxcalc/utils.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# pylint: disable=too-many-lines
99

1010
import os
11+
import re
1112
import math
1213
import json
1314
import copy
@@ -1574,25 +1575,45 @@ def bootstrap_se_ci(data, seed, num_samples, statistic, alpha):
15741575
return bsest
15751576

15761577

1577-
def json_to_dict(json_text):
1578+
def json_to_dict(jsontext):
15781579
"""
15791580
Convert specified JSON text into an ordered Python dictionary.
15801581
15811582
Parameters
15821583
----------
1583-
json_text: string
1584-
JSON text.
1584+
jsontext: string
1585+
JSON text that may contain comments, which will be removed
15851586
15861587
Raises
15871588
------
15881589
ValueError:
1589-
if json_text contains a JSON syntax error.
1590+
if jsontext contains a JSON syntax error after comments are removed
15901591
15911592
Returns
15921593
-------
15931594
dictionary: collections.OrderedDict
1594-
JSON data expressed as an ordered Python dictionary.
1595+
JSON data expressed as an ordered Python dictionary
15951596
"""
1597+
def remove_comments(string):
1598+
"""
1599+
Remove single and multiline comments from JSON.
1600+
Logic follows https://stackoverflow.com/a/18381470/9100772
1601+
"""
1602+
def _replacer(match):
1603+
# if the 2nd group (capturing comments) is not None,
1604+
# it means we have captured a non-quoted (real) comment string.
1605+
if match.group(2) is not None:
1606+
return "\n" # preserve line numbers
1607+
# otherwise, we will return the 1st group
1608+
return match.group(1) # captured quoted-string
1609+
# begin main remove_comments function logic
1610+
pattern = r"(\".*?\"|\'.*?\')|(/\*.*?\*/|//[^\r\n]*$)"
1611+
# first group captures quoted strings (double or single)
1612+
# second group captures comments (//single-line or /* multi-line */)
1613+
regex = re.compile(pattern, re.MULTILINE | re.DOTALL)
1614+
return regex.sub(_replacer, string)
1615+
# begin main json_to_dict function logic
1616+
json_text = remove_comments(jsontext)
15961617
try:
15971618
ordered_dict = json.loads(json_text,
15981619
object_pairs_hook=collections.OrderedDict)
@@ -1610,5 +1631,6 @@ def json_to_dict(json_text):
16101631
linenum += 1
16111632
msg += f'{linenum:04d}{line}\n'
16121633
msg += bline + '\n'
1634+
msg += 'If still puzzled, try using JSONLint online.\n'
16131635
raise ValueError(msg) from valerr
16141636
return ordered_dict

0 commit comments

Comments
 (0)