Start modeling Line, Form and their relation to TaxReturn.
authorRobert Sesek <rsesek@bluestatic.org>
Tue, 18 Feb 2020 03:57:49 +0000 (22:57 -0500)
committerRobert Sesek <rsesek@bluestatic.org>
Tue, 18 Feb 2020 03:57:49 +0000 (22:57 -0500)
src/Errors.ts [new file with mode: 0644]
src/Form.test.ts [new file with mode: 0644]
src/Form.ts
src/Line.ts [new file with mode: 0644]
src/TaxReturn.test.ts
src/TaxReturn.ts

diff --git a/src/Errors.ts b/src/Errors.ts
new file mode 100644 (file)
index 0000000..1c1ea80
--- /dev/null
@@ -0,0 +1,20 @@
+export class InconsistencyError extends Error {
+  constructor(m: string) {
+    super(m);
+    Object.setPrototypeOf(this, InconsistencyError.prototype);
+  }
+};
+
+export class UnsupportedFeatureError extends Error {
+  constructor(m: string) {
+    super(m);
+    Object.setPrototypeOf(this, UnsupportedFeatureError.prototype);
+  }
+};
+
+export class NotFoundError extends Error {
+  constructor(m: string) {
+    super(m);
+    Object.setPrototypeOf(this, NotFoundError.prototype);
+  }
+};
diff --git a/src/Form.test.ts b/src/Form.test.ts
new file mode 100644 (file)
index 0000000..24a93e6
--- /dev/null
@@ -0,0 +1,53 @@
+import { ComputedLine, Line } from './Line';
+import TaxReturn from './TaxReturn';
+import Form from './Form';
+import { InconsistencyError, NotFoundError } from './Errors';
+
+test('add and get line', () => {
+  const l = new ComputedLine<number>('1', () => 42);
+
+  class TestForm extends Form {
+    get name(): string {
+      return 'Test Form';
+    }
+
+    protected addLines() {
+      this.addLine(l);
+    }
+  };
+
+  const f = new TestForm();
+  expect(f.getLine('1')).toBe(l);
+});
+
+test('get non-existent line', () => {
+  class TestForm extends Form {
+    get name(): string {
+      return 'Test Form';
+    }
+
+    protected addLines() {
+    }
+  };
+
+  const f = new TestForm();
+  expect(() => f.getLine('1')).toThrow(NotFoundError);
+});
+
+test('add duplicate line', () => {
+  const l1 = new ComputedLine<number>('1', () => 42);
+  const l2 = new ComputedLine<number>('1', () => 36);
+
+  class TestForm extends Form {
+    get name(): string {
+      return 'Test Form';
+    }
+
+    protected addLines() {
+      this.addLine(l1);
+      this.addLine(l2);
+    }
+  };
+
+  expect(() => new TestForm()).toThrow(InconsistencyError);
+});
index 1e2cb9a40c44ffec11be4090ab5e2620d14a35a4..fe1493d5149b04d74af96c5d1ee6d50e8767c013 100644 (file)
@@ -1,2 +1,36 @@
-export default class Form {
+import { Line } from './Line';
+import { InconsistencyError, NotFoundError } from './Errors';
+
+export default abstract class Form {
+  private _lines: Line<any>[] = [];
+
+  abstract get name(): string;
+
+  constructor() {
+    this.addLines();
+  }
+
+  protected abstract addLines(): void;
+
+  get allowMultipleCopies(): boolean {
+    return false;
+  }
+
+  protected addLine(line: Line<any>) {
+    try {
+      this.getLine(line.id);
+    } catch {
+      this._lines.push(line);
+      return;
+    }
+    throw new InconsistencyError('Cannot add a line with a duplicate identifier');
+  }
+
+  getLine(id: string): Line<any> {
+    const lines = this._lines.filter(l => l.id === id);
+    if (lines.length == 0) {
+      throw new NotFoundError(id);
+    }
+    return lines[0];
+  }
 };
diff --git a/src/Line.ts b/src/Line.ts
new file mode 100644 (file)
index 0000000..e395d0b
--- /dev/null
@@ -0,0 +1,51 @@
+import TaxReturn from './TaxReturn';
+
+export abstract class Line<T> {
+  private _id: string;
+  private _description?: string;
+
+  constructor(id: string, description?: string) {
+    this._id = id;
+    this._description = description;
+  }
+
+  get id(): string {
+    return this._id;
+  }
+
+  get description(): string {
+    return this._description;
+  }
+
+  abstract value(tr: TaxReturn): T;
+};
+
+type ComputeFunc<T> = (tr: TaxReturn, l: ComputedLine<T>) => T;
+
+export class ComputedLine<T> extends Line<T> {
+  private _compute: ComputeFunc<T>;
+
+  constructor(id: string, compute: ComputeFunc<T>, description?: string) {
+    super(id, description);
+    this._compute = compute;
+  }
+
+  value(tr: TaxReturn): T {
+    return this._compute(tr, this);
+  }
+};
+
+export class ReferenceLine<T> extends Line<T> {
+  private _form: string;
+  private _line: string;
+
+  constructor(id: string, form: string, line: string) {
+    super(id, `Reference F${form}.L${line}`);
+    this._form = form;
+    this._line = line;
+  }
+
+  value(tr: TaxReturn): T {
+    return tr.getForm(this._form).getLine(this._line).value(tr);
+  }
+};
index 0c0aab6ec3a44dad17751f9dbcd2c53e5320bd8b..e6a616508e8548770454895a580a3a5612509c52 100644 (file)
@@ -1,5 +1,7 @@
 import TaxReturn from './TaxReturn';
 import { Person } from './Person';
+import Form from './Form';
+import { NotFoundError, InconsistencyError } from './Errors';
 
 test('constructor', () => {
   const tr = new TaxReturn(2019);
@@ -49,3 +51,48 @@ test('get non-existent person', () => {
   expect(tr.getPerson('Billy Bob')).toBe(self);
   expect(() => tr.getPerson('Jilly')).toThrow('not found');
 });
+
+test('single-copy forms', () => {
+  class TestForm extends Form {
+    get name(): string { return 'Test Form'; }
+
+    protected addLines() {}
+  };
+
+  const tr = new TaxReturn(2019);
+  const f = new TestForm();
+  tr.addForm(f);
+  expect(() => tr.addForm(new TestForm)).toThrow(InconsistencyError);
+  expect(tr.getForm(f.name)).toBe(f);
+});
+
+test('multiple-copy forms', () => {
+  class TestForm extends Form {
+    get name(): string { return 'Test Form'; }
+
+    get allowMultipleCopies(): boolean { return true; }
+
+    protected addLines() {}
+  };
+
+  const tr = new TaxReturn(2019);
+  const f1 = new TestForm();
+  const f2 = new TestForm();
+  const f3 = new TestForm();
+  tr.addForm(f1);
+  tr.addForm(f2);
+
+  expect(() => tr.getForm(f1.name)).toThrow(InconsistencyError);
+
+  const forms = tr.getForms(f1.name);
+  expect(forms.length).toBe(2);
+  expect(forms).toContain(f1);
+  expect(forms).toContain(f2);
+  expect(forms).not.toContain(f3);
+});
+
+test('get non-existent form', () => {
+  const tr = new TaxReturn(2019);
+  expect(() => tr.getForm('form')).toThrow(NotFoundError);
+  expect(tr.getForms('form')).toEqual([]);
+});
index 192a41e20475b868d9fc74f466e5bb623465be48..1614a1c23e826704866939a630cb6e9c039e0292 100644 (file)
@@ -1,5 +1,6 @@
 import Form from './Form';
 import { Person, Relation } from './Person';
+import { NotFoundError, InconsistencyError, UnsupportedFeatureError } from './Errors';
 
 export default class TaxReturn {
   private _year: number;
@@ -36,11 +37,27 @@ export default class TaxReturn {
   }
 
   addForm(form: Form) {
+    if (!form.allowMultipleCopies) {
+      const other = this.getForms(form.name);
+      if (other.length > 0) {
+        throw new InconsistencyError(`Cannot have more than one type of form ${form.name}`);
+      }
+    }
+    this._forms.push(form);
   }
-};
 
-export class InconsistencyError extends Error {
-};
+  getForm(name: string): Form {
+    const forms = this.getForms(name);
+    if (forms.length == 0) {
+      throw new NotFoundError(`No form named ${name}`);
+    }
+    if (forms.length > 1) {
+      throw new InconsistencyError(`More than 1 form named ${name}`);
+    }
+    return forms[0];
+  }
 
-export class UnsupportedFeatureError extends Error {
+  getForms(name: string): Form[] {
+    return this._forms.filter(f => f.name == name);
+  }
 };