Skip to content

Commit 80d4470

Browse files
authored
Merge pull request #134 from rchillyard/cats
Cats with a few other non-cats issues fixed (although these didn't have Issue numbers)
2 parents b3b2546 + 0a1a8fb commit 80d4470

File tree

65 files changed

+3736
-2376
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+3736
-2376
lines changed

build.sbt

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ organization := "com.phasmidsoftware"
22

33
name := "Number"
44

5-
version := "1.2.10"
5+
version := "1.2.11"
66

77
scalaVersion := "2.13.16"
88

@@ -13,14 +13,22 @@ scalacOptions ++= Seq("-encoding", "UTF-8", "-unchecked", "-deprecation", "-Ywar
1313
val scalaTestVersion = "3.2.19"
1414

1515
libraryDependencies ++= Seq(
16-
"com.phasmidsoftware" %% "flog" % "1.0.10",
17-
"com.phasmidsoftware" %% "matchers" % "1.0.11",
18-
"org.apache.commons" % "commons-math3" % "3.6.1",
19-
"org.scala-lang.modules" %% "scala-parser-combinators" % "2.4.0",
20-
"com.novocode" % "junit-interface" % "0.11" % "test", // NOTE vulnerability here
21-
"org.scalatest" %% "scalatest" % scalaTestVersion % "test",
22-
"ch.qos.logback" % "logback-classic" % "1.5.19" % "test",
23-
"org.scalacheck" %% "scalacheck" % "1.19.0" % "test" // This is used for testing Rational
16+
17+
"com.phasmidsoftware" %% "flog" % "1.0.10",
18+
"com.phasmidsoftware" %% "matchers" % "1.0.11",
19+
"org.apache.commons" % "commons-math3" % "3.6.1",
20+
"com.novocode" % "junit-interface" % "0.11" % "test", // NOTE vulnerability here
21+
"org.scalatest" %% "scalatest" % scalaTestVersion % "test",
22+
"org.scala-lang.modules" %% "scala-parser-combinators" % "2.4.0",
23+
"ch.qos.logback" % "logback-classic" % "1.5.19" % "test",
24+
"org.scalacheck" %% "scalacheck" % "1.19.0" % "test", // This is used for testing Rational
25+
"org.typelevel" %% "cats-laws" % "2.10.0" % "test",
26+
"org.typelevel" %% "discipline-scalatest" % "2.3.0" % "test",
27+
"org.typelevel" %% "spire" % "0.18.0",
28+
"org.typelevel" %% "cats-kernel" % "2.10.0",
29+
"org.typelevel" %% "cats-core" % "2.10.0"
30+
31+
2432
)
2533

2634
resolvers += "Typesafe Repository" at "https://repo.typesafe.com/typesafe/releases/"

publish.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ ThisBuild / publishMavenStyle := true
1616
ThisBuild / credentials += Credentials(Path.userHome / ".sbt" / "sonatype_credentials")
1717

1818
// Required POM metadata for Maven Central
19-
ThisBuild / licenses := List("MIT" -> url("http://opensource.org/licenses/MIT"))
19+
ThisBuild / licenses := List("MIT" -> url("https://opensource.org/licenses/MIT"))
2020
ThisBuild / scmInfo := Some(
2121
ScmInfo(
2222
url("https://github.com/rchillyard/Number"),
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright (c) 2025. Phasmid Software
3+
*/
4+
5+
package com.phasmidsoftware.number.cats
6+
7+
import com.phasmidsoftware.number.cats.ErrorCommutativeMonoid._
8+
import com.phasmidsoftware.number.core.inner.{PureNumber, Value}
9+
import com.phasmidsoftware.number.core.{AbsoluteFuzz, FuzzyNumber, Gaussian, Number, RelativeFuzz}
10+
11+
/**
12+
* Usage-focused examples for the error metric Monoid instances.
13+
*
14+
* These tests are intentionally lightweight demonstrations rather than law checks.
15+
*/
16+
class ErrorCommutativeMonoidFuncSpec extends AnyFlatSpec with Matchers {
17+
18+
behavior of "Abstracting advocacy communication into lawful scalar folding"
19+
20+
21+
22+
it should "match decoupled parallel error folding with direct Number addition (all addition)" in {
23+
implicit val ec: ExecutionContext = ExecutionContext.global
24+
25+
// Build many fuzzy addends: same nominal 1.2 with absolute Gaussian sigma 0.05
26+
val terms: List[Number] = List.fill(2000) {
27+
FuzzyNumber(Value.fromDouble(Some(1.2)), PureNumber, Some(AbsoluteFuzz(0.05, Gaussian)))
28+
}
29+
30+
// 1) Traditional: add fuzzy Numbers directly (measure time)
31+
val (accumulated: Number, tSeqMs) = 1.times {
32+
terms.tail.foldLeft(terms.head)(_ doAdd _)
33+
}
34+
35+
val actualNominal = accumulated.toNominalDouble.getOrElse(Double.NaN)
36+
val actualAbs = accumulated match {
37+
case f: FuzzyNumber => f.fuzz.collect { case AbsoluteFuzz(m: Double, Gaussian) => m }.getOrElse(Double.NaN)
38+
case _ => Double.NaN
39+
}
40+
41+
// 2) Decoupled parallel: nominal sum and sigma folding run independently
42+
val ((decoupledNominal, decoupledSigma), tParMs) = 1.times {
43+
val fNominal: Future[Double] = Future { terms.flatMap(_.toNominalDouble).sum }
44+
val fSigma: Future[Double] = Future {
45+
val sigmas = terms.map {
46+
case f: FuzzyNumber => f.fuzz.collect { case AbsoluteFuzz(m: Double, Gaussian) => m }.getOrElse(0.0)
47+
case _ => 0.0
48+
}
49+
sigmas.foldLeft(AbsSigma.zero)(_ |+| AbsSigma(_)).value
50+
}
51+
val a = Await.result(fNominal, 5.seconds)
52+
val b = Await.result(fSigma, 5.seconds)
53+
(a, b)
54+
}
55+
56+
val decoupled: Number = FuzzyNumber(Value.fromDouble(Some(decoupledNominal)), PureNumber, Some(AbsoluteFuzz(decoupledSigma, Gaussian)))
57+
58+
val decoupledAbs = decoupled match {
59+
case f: FuzzyNumber => f.fuzz.collect { case AbsoluteFuzz(m: Double, Gaussian) => m }.getOrElse(Double.NaN)
60+
case _ => Double.NaN
61+
}
62+
63+
// Compare nominal and absolute sigma
64+
actualNominal shouldBe decoupledNominal +- 1e-9
65+
actualAbs shouldBe decoupledAbs +- 1e-9
66+
67+
// And new method should be faster (or equal) than traditional
68+
// NOTE: simple wall-clock comparison; if flaky in CI, relax or increase data size
69+
// tParMs: 72.449458, tSeqMs: 194.883
70+
println(s"tParMs: $tParMs, tSeqMs: $tSeqMs")
71+
assert(tParMs <= tSeqMs, s"decoupled parallel folding should be faster: par=${tParMs}ms vs seq=${tSeqMs}ms")
72+
}
73+
74+
// CONSIDER this test is slow. We might want to tag it as Slow (or perhaps try to speed it up)
75+
it should "match decoupled parallel error folding with direct Number multiplication (all multiplication)" in {
76+
implicit val ec: ExecutionContext = ExecutionContext.global
77+
78+
// Build many fuzzy addends: same nominal 1.2 with absolute Gaussian sigma 0.05
79+
val terms: List[Number] = List.fill(2000) {
80+
FuzzyNumber(Value.fromDouble(Some(1.1)), PureNumber, Some(RelativeFuzz(0.05, Gaussian)))
81+
}
82+
83+
// 1) Traditional: multiply fuzzy Numbers directly (measure time)
84+
val (accumulated: Number, tSeqMs) = 1.times {
85+
terms.tail.foldLeft(terms.head)(_ doMultiply _)
86+
}
87+
88+
val actualNominal = accumulated.toNominalDouble.getOrElse(Double.NaN)
89+
val actualRel = accumulated match {
90+
case f: FuzzyNumber => f.fuzz.collect { case RelativeFuzz(m: Double, Gaussian) => m }.getOrElse(Double.NaN)
91+
case _ => Double.NaN
92+
}
93+
94+
// 2) Decoupled parallel: nominal product and sigma folding run independently
95+
val ((decoupledNominal, decoupledSigma), tParMs) = 1.times {
96+
val fNominal: Future[Double] = Future {
97+
val headNominal = terms.headOption.flatMap(_.toNominalDouble).getOrElse(Double.NaN)
98+
terms.tail.foldLeft(headNominal) { (acc, n) =>
99+
acc * n.toNominalDouble.getOrElse(1.0)
100+
}
101+
}
102+
val fSigma: Future[Double] = Future {
103+
// Sequential combination, relative basis propagation: r_xy^2 = r_x^2 + r_y^2 + r_x r_y
104+
def combineRel(r1: Double, r2: Double): Double = {
105+
val a = r1; val b = r2
106+
math.sqrt(a * a + b * b + a * b)
107+
}
108+
val sigmas = terms.map {
109+
case f: FuzzyNumber => f.fuzz.collect { case RelativeFuzz(m: Double, Gaussian) => m }.getOrElse(0.0)
110+
case _ => 0.0
111+
}
112+
sigmas.foldLeft(0.0)(combineRel)
113+
}
114+
val a = Await.result(fNominal, 5.seconds)
115+
val b = Await.result(fSigma, 5.seconds)
116+
(a, b)
117+
}
118+
119+
val decoupled: Number = FuzzyNumber(Value.fromDouble(Some(decoupledNominal)), PureNumber, Some(RelativeFuzz(decoupledSigma, Gaussian)))
120+
121+
val decoupledRel = decoupled match {
122+
case f: FuzzyNumber => f.fuzz.collect { case RelativeFuzz(m: Double, Gaussian) => m }.getOrElse(Double.NaN)
123+
case _ => Double.NaN
124+
}
125+
126+
// Compare nominal and relative sigma
127+
math.abs(actualNominal - decoupledNominal) / math.abs(actualNominal) should be < 1e-12
128+
actualRel shouldBe decoupledRel +- 1e-1
129+
130+
// And new method should be faster (or equal) than traditional
131+
// NOTE: simple wall-clock comparison; if flaky in CI, relax or increase data size
132+
// tParMs: 5.240125, tSeqMs: 7731.776417
133+
println(s"tParMs: $tParMs, tSeqMs: $tSeqMs")
134+
assert(tParMs <= tSeqMs, s"decoupled parallel folding should be faster: par=${tParMs}ms vs seq=${tSeqMs}ms")
135+
}
136+
137+
}
138+
139+

src/it/scala/com/phasmidsoftware/number/core/PrimesFuncSpec.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import org.scalatest.flatspec.AnyFlatSpec
44
import org.scalatest.matchers.should
55

66
/**
7-
* CONSIDER moving this to an integration testing directory since it is a bit slow.
8-
*/
7+
* CONSIDER moving this to an integration testing directory since it is a bit slow.
8+
*/
99
class PrimesFuncSpec extends AnyFlatSpec with should.Matchers {
1010

1111
behavior of "Prime"

src/it/scala/com/phasmidsoftware/number/core/SeriesFuncSpec.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import org.scalatest.flatspec.AnyFlatSpec
99
import org.scalatest.matchers.should.Matchers
1010
import scala.util.Random
1111

12-
class SeriesFuncSpec extends AnyFlatSpec with Matchers with FuzzyEquality{
12+
class SeriesFuncSpec extends AnyFlatSpec with Matchers with FuzzyEquality {
1313
behavior of "Basel Problem"
1414

1515
val basel: InfiniteSeries[Number] = InfiniteSeries(LazyList.from(1).map(x => Rational(x).invert.square))

src/main/java/com/phasmidsoftware/number/core/BigNumber.java

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public static BigNumber value(final Rational x) {
9292
* TESTME
9393
*
9494
* @param whole a non-negative long.
95-
* @param decimals a non-negative long, for example 14 for a 3.14 valued result.
95+
* @param decimals a non-negative long, for example, 14 for a 3.14 valued result.
9696
* @param sign true if the result should be positive.
9797
* @return a new BigNumber.
9898
*/
@@ -115,7 +115,7 @@ public static BigNumber value(final long whole, final long decimals, final boole
115115
* TESTME
116116
*
117117
* @param whole a non-negative long.
118-
* @param decimals a non-negative long, for example 14 for a 3.14 valued result.
118+
* @param decimals a non-negative long, for example, 14 for a 3.14 valued result.
119119
* @return a new BigNumber.
120120
*/
121121
public static BigNumber value(final long whole, final long decimals) {
@@ -275,7 +275,6 @@ public BigNumber add(final BigNumber that) {
275275
final boolean subtract = !that.sign;
276276
for (int i = resultLength - 1; i >= 0; i--) {
277277
int sum = carry;
278-
borrow = false;
279278
if (i < thisLength) sum += decimals[i];
280279
if (i < thatLength) sum += subtract ? -that.decimals[i] : that.decimals[i];
281280
if (sum < 0) {
@@ -303,23 +302,22 @@ public BigNumber add(final BigNumber that) {
303302
* @return a negative, zero, or positive int.
304303
*/
305304
public int compareTo(final BigNumber that) {
306-
if (this.equals(that)) {
305+
if (this.equals(that))
307306
return 0;
308-
} else {
307+
else {
309308
int wholeComparison = this.whole.compareTo(that.whole);
310-
if (wholeComparison != 0) {
309+
if (wholeComparison != 0)
311310
return this.sign ? wholeComparison : -wholeComparison;
312-
} else {
311+
else {
313312
final int[] thisDecimals = this.decimals;
314313
final int[] thatDecimals = that.decimals;
315314
final int maxLength = Math.max(thisDecimals.length, thatDecimals.length);
316315
for (int i = 0; i < maxLength; i++) {
317316
int thisNumber = (i < thisDecimals.length) ? thisDecimals[i] : 0;
318317
int thatNumber = (i < thatDecimals.length) ? thatDecimals[i] : 0;
319-
if (thisNumber != thatNumber) {
318+
if (thisNumber != thatNumber)
320319
return this.sign ? Integer.compare(thisNumber, thatNumber)
321320
: Integer.compare(thatNumber, thisNumber);
322-
}
323321
}
324322
return 0;
325323
}
@@ -374,10 +372,10 @@ public BigNumber multiplyWithKaratsuba(final BigNumber other) {
374372
* Multiplies two integer arrays representing numerical values and returns the result in an array.
375373
* TESTME
376374
*
377-
* @param arr1 the first integer array representing a number
378-
* @param arr2 the second integer array representing a number
379-
* @param start the starting index for the portion of the arrays to be considered
380-
* @param end the ending index for the portion of the arrays to be considered
375+
* @param arr1 the first integer array representing a number
376+
* @param arr2 the second integer array representing a number
377+
* @param start the starting index for the portion of the arrays to be considered
378+
* @param end the ending index for the portion of the arrays to be considered
381379
* @param resultSize the size of the resulting array
382380
* @return an integer array representing the result of the multiplication
383381
*/
@@ -410,10 +408,10 @@ private int[] multiplyArrays(final int[] arr1, final int[] arr2, final int start
410408
* efficient computation of the product.
411409
* TESTME
412410
*
413-
* @param first The first integer array, where each element represents part of a larger number.
414-
* @param second The second integer array, where each element represents part of a larger number.
415-
* @param start The starting index for the relevant segment of the arrays to be considered for multiplication.
416-
* @param end The ending index for the relevant segment of the arrays to be considered for multiplication.
411+
* @param first The first integer array, where each element represents part of a larger number.
412+
* @param second The second integer array, where each element represents part of a larger number.
413+
* @param start The starting index for the relevant segment of the arrays to be considered for multiplication.
414+
* @param end The ending index for the relevant segment of the arrays to be considered for multiplication.
417415
* @param resSize The size of the result array to store the product of the two arrays.
418416
* @return An array representing the product of the two numbers, with the result properly sized as per resSize.
419417
*/
@@ -460,7 +458,7 @@ private int[] recursiveKarat(final int[] first, final int[] second, final int st
460458
* Zeros are appended to the end of the array to maintain the original length.
461459
* TESTME
462460
*
463-
* @param arr the array of integers to be shifted; must not be null
461+
* @param arr the array of integers to be shifted; must not be null
464462
* @param offset the number of positions to shift the elements to the left; must be non-negative
465463
*/
466464
private void leftShift(final int[] arr, final int offset) {
@@ -479,10 +477,10 @@ private void leftShift(final int[] arr, final int offset) {
479477
* and stores the result in a new array of specified size.
480478
* TESTME
481479
*
482-
* @param first the input array from which elements are added
483-
* @param start the starting index of the first part in the array
484-
* @param middle the ending index of the first part in the array
485-
* @param end the ending index of the second part in the array
480+
* @param first the input array from which elements are added
481+
* @param start the starting index of the first part in the array
482+
* @param middle the ending index of the first part in the array
483+
* @param end the ending index of the second part in the array
486484
* @param resSize the size of the resulting array
487485
* @return an array containing the result of the addition with carry
488486
*/
@@ -516,8 +514,8 @@ private int[] add(final int[] first, final int start, final int middle, final in
516514
* Handles carry values appropriately during addition.
517515
* TESTME
518516
*
519-
* @param first the array representing the first number. Must be large enough
520-
* to accommodate the result of the addition.
517+
* @param first the array representing the first number. Must be large enough
518+
* to accommodate the result of the addition.
521519
* @param second the array representing the second number. Its digits will be
522520
* added to the corresponding digits of the first array.
523521
*/
@@ -646,14 +644,13 @@ public BigNumber divide(final long x) {
646644
*
647645
* @param o the object to be compared for equality with this BigNumber
648646
* @return true if the specified object is a BigNumber, has the same whole part,
649-
* sign, and decimals as this BigNumber; otherwise, false
647+
* sign, and decimals as this BigNumber; otherwise, false
650648
*/
651649
@Override
652650
public boolean equals(final Object o) {
653651
if (this == o) return true;
654652
// NOTE That if you replace this as recommended, you must ensure that Java is compiling on CircleCI with the correct compiler (Java 16).
655-
if ( !(o instanceof BigNumber ) ) return false;
656-
final BigNumber bigNumber = (BigNumber) o;
653+
if (!(o instanceof BigNumber bigNumber)) return false;
657654
return whole.equals(bigNumber.whole) && sign == bigNumber.sign && Arrays.equals(decimals, bigNumber.decimals);
658655
}
659656

@@ -703,7 +700,7 @@ public BigNumber(final BigInteger whole) {
703700
}
704701

705702
/**
706-
* Secondary constructor to creat a BigNumber which has no decimal part.
703+
* Secondary constructor to create a BigNumber which has no decimal part.
707704
*
708705
* @param whole any BigInteger value (positive or negative).
709706
*/
@@ -731,7 +728,7 @@ private static int[] trim(final int[] array) {
731728
* if positive, retrieves the corresponding decimal index;
732729
* if negative, returns 0.
733730
* @return the element value as a long; the whole number if the index is 0,
734-
* the decimal value if the index is positive, or 0 if the index is negative.
731+
* the decimal value if the index is positive, or 0 if the index is negative.
735732
*/
736733
private long element(final int i) {
737734
if (i == 0) return whole.longValueExact();
@@ -768,15 +765,15 @@ public static void main(final String[] args) {
768765
/**
769766
* Measures and compares the performance of two multiplication algorithms (standard and Karatsuba) for large numbers.
770767
* TESTME
771-
*
768+
* <p>
772769
* The method performs the following steps:
773770
* 1. Initializes a large constant string representing a seed value and extracts a portion for computation.
774771
* 2. Parses the seed string into a `BigNumber` object for arithmetic operations.
775772
* 3. Uses a loop to measure the time taken to execute multiplication using the standard multiplication method.
776773
* 4. Computes the average execution time for the standard multiplication method.
777774
* 5. Repeats the process for the Karatsuba multiplication method, measuring and calculating average execution time.
778-
* 6. Prints the execution times for both methods, and computes the percentage improvement offered by the Karatsuba method.
779-
*
775+
* 6. Prints the execution times for both methods and computes the percentage improvement offered by the Karatsuba method.
776+
* <p>
780777
* Notes:
781778
* - Considers the potential use of performance measurement utilities such as `StopWatch` or `Timer`.
782779
* - Ensures timing accuracy by iterating over each method multiple times and averaging the results.

0 commit comments

Comments
 (0)