Compare commits
1 Commits
main
...
agentd/tas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
724ee92a2b |
@@ -16,6 +16,8 @@ public static class PriceCalculator
|
||||
throw new ArgumentOutOfRangeException(nameof(discountPercent), "A discount is between 0 and 100 percent.");
|
||||
}
|
||||
|
||||
return price - discountPercent;
|
||||
var discounted = price * (1 - discountPercent / 100m);
|
||||
// Swiss rounding (Rappenrundung): round to the nearest 0.05
|
||||
return Math.Round(discounted / 0.05m, MidpointRounding.ToEven) * 0.05m;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,4 +33,25 @@ public sealed class PriceCalculatorTests
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => PriceCalculator.ApplyDiscount(100m, 101m));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyDiscount_99_90_with_10_percent_rounds_to_89_90()
|
||||
{
|
||||
// 99.90 * (1 - 10/100) = 89.91 → rounded to nearest 0.05 = 89.90
|
||||
Assert.Equal(89.90m, PriceCalculator.ApplyDiscount(99.90m, 10m));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyDiscount_10_with_33_percent_rounds_correctly()
|
||||
{
|
||||
// 10 * (1 - 33/100) = 6.70 → 6.70 is already a multiple of 0.05
|
||||
Assert.Equal(6.70m, PriceCalculator.ApplyDiscount(10m, 33m));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyDiscount_5_55_with_10_percent_rounds_to_5_00()
|
||||
{
|
||||
// 5.55 * 0.9 = 4.995 → rounded to nearest 0.05 = 5.00
|
||||
Assert.Equal(5.00m, PriceCalculator.ApplyDiscount(5.55m, 10m));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
</defs>
|
||||
</svg>
|
||||
<h1>Hello, {{ title() }}</h1>
|
||||
<app-discount-calculator />
|
||||
<p>Congratulations! Your app is running. 🎉</p>
|
||||
</div>
|
||||
<div class="divider" role="separator" aria-label="Divider"></div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { DiscountCalculator } from './discount-calculator/discount-calculator';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet],
|
||||
imports: [RouterOutlet, DiscountCalculator],
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.scss'
|
||||
})
|
||||
|
||||
74
web/src/app/discount-calculator/discount-calculator.spec.ts
Normal file
74
web/src/app/discount-calculator/discount-calculator.spec.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DiscountCalculator } from './discount-calculator';
|
||||
|
||||
describe('DiscountCalculator', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DiscountCalculator, FormsModule],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the component', () => {
|
||||
const fixture = TestBed.createComponent(DiscountCalculator);
|
||||
const component = fixture.componentInstance;
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display "CHF 89.90" for price=99.90 and discount=10', async () => {
|
||||
const fixture = TestBed.createComponent(DiscountCalculator);
|
||||
fixture.detectChanges();
|
||||
|
||||
const priceInput = fixture.nativeElement.querySelector('#price') as HTMLInputElement;
|
||||
const discountInput = fixture.nativeElement.querySelector('#discount') as HTMLInputElement;
|
||||
|
||||
priceInput.value = '99.90';
|
||||
priceInput.dispatchEvent(new Event('input'));
|
||||
discountInput.value = '10';
|
||||
discountInput.dispatchEvent(new Event('input'));
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const resultEl = fixture.nativeElement.querySelector('.result-value') as HTMLElement;
|
||||
expect(resultEl.textContent?.trim()).toBe('CHF 89.90');
|
||||
});
|
||||
|
||||
it('should display "CHF 100.00" for price=100 and discount=0', async () => {
|
||||
const fixture = TestBed.createComponent(DiscountCalculator);
|
||||
fixture.detectChanges();
|
||||
|
||||
const priceInput = fixture.nativeElement.querySelector('#price') as HTMLInputElement;
|
||||
const discountInput = fixture.nativeElement.querySelector('#discount') as HTMLInputElement;
|
||||
|
||||
priceInput.value = '100';
|
||||
priceInput.dispatchEvent(new Event('input'));
|
||||
discountInput.value = '0';
|
||||
discountInput.dispatchEvent(new Event('input'));
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const resultEl = fixture.nativeElement.querySelector('.result-value') as HTMLElement;
|
||||
expect(resultEl.textContent?.trim()).toBe('CHF 100.00');
|
||||
});
|
||||
|
||||
it('should display "CHF 150.00" for price=200 and discount=25', async () => {
|
||||
const fixture = TestBed.createComponent(DiscountCalculator);
|
||||
fixture.detectChanges();
|
||||
|
||||
const priceInput = fixture.nativeElement.querySelector('#price') as HTMLInputElement;
|
||||
const discountInput = fixture.nativeElement.querySelector('#discount') as HTMLInputElement;
|
||||
|
||||
priceInput.value = '200';
|
||||
priceInput.dispatchEvent(new Event('input'));
|
||||
discountInput.value = '25';
|
||||
discountInput.dispatchEvent(new Event('input'));
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const resultEl = fixture.nativeElement.querySelector('.result-value') as HTMLElement;
|
||||
expect(resultEl.textContent?.trim()).toBe('CHF 150.00');
|
||||
});
|
||||
});
|
||||
135
web/src/app/discount-calculator/discount-calculator.ts
Normal file
135
web/src/app/discount-calculator/discount-calculator.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-discount-calculator',
|
||||
standalone: true,
|
||||
imports: [FormsModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="card">
|
||||
<h2>Rabatt-Rechner</h2>
|
||||
<div class="field">
|
||||
<label for="price">Preis (CHF)</label>
|
||||
<input
|
||||
id="price"
|
||||
type="number"
|
||||
step="0.05"
|
||||
[ngModel]="priceValue()"
|
||||
(ngModelChange)="priceValue.set($event)"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="discount">Rabatt (%)</label>
|
||||
<input
|
||||
id="discount"
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
max="100"
|
||||
[ngModel]="discountValue()"
|
||||
(ngModelChange)="discountValue.set($event)"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="result">
|
||||
<span class="result-label">Endpreis</span>
|
||||
<span class="result-value">{{ displayValue() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: `
|
||||
.card {
|
||||
max-width: 320px;
|
||||
margin: 2rem auto;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #d0d0d0;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 1.25rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.875rem;
|
||||
color: #555;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.15);
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: #e9f5e9;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.result-label {
|
||||
font-size: 0.875rem;
|
||||
color: #2a6b2a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #1a4a1a;
|
||||
}
|
||||
`
|
||||
})
|
||||
export class DiscountCalculator {
|
||||
protected readonly priceValue = signal<number | null>(null);
|
||||
protected readonly discountValue = signal<number | null>(null);
|
||||
|
||||
protected readonly displayValue = computed(() => {
|
||||
const price = this.priceValue();
|
||||
const discount = this.discountValue();
|
||||
|
||||
if (price === null || discount === null) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
if (isNaN(price) || isNaN(discount)) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
if (price <= 0) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
if (discount < 0 || discount > 100) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
const discounted = price * (1 - discount / 100);
|
||||
const rounded = Math.round(discounted / 0.05) * 0.05;
|
||||
return `CHF ${rounded.toFixed(2)}`;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user