diff --git a/project/build.properties b/project/build.properties index 875b706..04267b1 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.2 +sbt.version=1.9.9 diff --git a/project/plugins.sbt b/project/plugins.sbt index 9dd1911..666719f 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1 +1 @@ -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") diff --git a/src/main/scala/example/Main.scala b/src/main/scala/example/Main.scala index e1915c0..3ee6070 100644 --- a/src/main/scala/example/Main.scala +++ b/src/main/scala/example/Main.scala @@ -7,6 +7,9 @@ import org.scalajs.dom import scala.scalajs.js.annotation.JSImport import scalajs.js +import java.text.DecimalFormat +import org.scalajs.dom.intl.NumberFormat +import com.raquo.airstream.core.Observable trait ChartDataset extends js.Any: var label: String @@ -42,10 +45,45 @@ case class Settings( val effectiveMonthlyRate = rate / 100 / 12 val thing = math.pow(1 + effectiveMonthlyRate, months) loanAmount * effectiveMonthlyRate * thing / (thing - 1) + + def stampDuty: Double = + val firstTimeBuyerRelief = housePrice <= 625_000 + val bands = + if !firstTimeBuyerRelief then + List( + 250_000 -> 0.0, + 925_000 -> 0.05, + 1_500_000 -> 0.10, + 100_000_000 -> 0.12 + ) + else List(425_000 -> 0.0, 625_000 -> 0.05) + + val augmentedBands = + bands + .prepended(0 -> 0.0) + .zip(bands) + .map: + case ((st, _), (ed, rate)) => + (st, ed) -> rate + + augmentedBands + .map: + case ((from, to), rate) => + if from < housePrice then + val length = to - from + val baseline = (housePrice - from) + if baseline > length then length * rate + else baseline * rate + else 0.0 + .sum + + end stampDuty + + def LTVPercent = 100 * (loanAmount / housePrice) end Settings def app = - val housePrice = Var(400_000.0) + val housePrice = Var(800_000.0) val deposit = Var(50_000.0) val durationYears = Var(25) val rate = Var(6.0) @@ -55,8 +93,12 @@ def app = val calculation = combined.map(_.monthlyPayment) - inline def poundValue(source: Source[Double]) = - span(color.maroon, child.text <-- source.toObservable.map(d => f"£$d%.2f")) + val numberFormat = NumberFormat("en-GB") + + inline def poundValue(source: Observable[Double]) = + val nn = + source.toObservable.map(value => "£" + numberFormat.format(value.floor)) + span(color.maroon, child.text <-- nn) val sliders = div( width := "50%", @@ -71,7 +113,7 @@ def app = _.showTickmarks := true, _.events.onInput.map(_.target.value) --> rate ), - h1("House price: ", poundValue(housePrice)), + h1("House price: ", poundValue(housePrice.signal)), Slider( _.min := 100_000, _.max := 1_200_000, @@ -82,7 +124,7 @@ def app = _.showTickmarks := true, _.events.onInput.map(_.target.value) --> housePrice ), - h1("Deposit: ", poundValue(deposit)), + h1("Deposit: ", poundValue(deposit.signal)), Slider( _.min := 10_000, _.max := 500_000, @@ -117,6 +159,14 @@ def app = ) val chart = div( width := "50%", + h2( + b("LTV: "), + child.text <-- combined.map(_.LTVPercent.toString() + "%") + ), + h2( + b("Stamp duty: "), + poundValue(combined.map(_.stampDuty)) + ), canvasTag( // onMountUnmount callback to bridge the Laminar world and the Chart.js world onMountUnmountCallback( // on mount, create the `Chart` instance and store it in optChart @@ -149,6 +199,7 @@ def app = } ) ) + div( h1( "The lolno calculator - why you will never afford a mortgage in the UK"