Thank you for your interest in contributing to Number! This guide will help you get started with development and understand the project's architecture and conventions.
- Scala 3.7.3 (or later 3.x version)
- sbt (Scala Build Tool)
- JDK 17 or later
- Git
-
Clone the repository:
git clone https://github.com/rchillyard/Number.git cd Number -
Build the project:
sbt compile
-
Run all tests:
sbt test -
Generate documentation:
sbt unidoc
The unified documentation will be generated in
target/scala-3.7.3/unidoc/
IntelliJ IDEA (recommended):
- Install the Scala plugin
- Import the project as an sbt project
- The IDE will automatically detect the module structure
VS Code:
- Install the Metals extension
- Open the project folder
- Metals will prompt you to import the build
Number is organized into multiple modules, each with a specific purpose:
Number (root)
βββ core - Legacy numeric types (Rational, Factor, Fuzz, etc.)
βββ algebra - Modern algebraic structures based on Cats typeclasses
βββ expression - Lazy expression evaluation
βββ parse - Parsing facilities for expressions and algebraic structures
βββ dimensions - Typesafe dimensional quantities and units
βββ top - High-level API and examples
Dependency flow: core β algebra β expression β parse β top
The dimensions module depends on algebra and core.
-
Exact arithmetic by default: Number maintains exactness wherever possible. Loss of precision requires explicit action (e.g., calling
.fuzzyor.materialize) -
Eager vs Lazy evaluation:
- Eager (
Eager,Solution,Complex, etc.): Values are evaluated immediately - Lazy (
Expression): Symbolic expressions that defer evaluation
- Eager (
-
Symbolic computation: Mathematical constants like Ο, e, β2 are kept symbolic until materialization is requested
-
Factor systems: Different numeric domains (roots, logarithms, angles) are represented through factors
+ operator can accidentally concatenate strings instead of performing arithmetic.
Number has strict compiler settings to catch this.
DON'T:
val x = someNumber + "accidentally a string" // Compiles to string concatenation!DO:
import com.phasmidsoftware.number.core.Number.NumberOps
val x = 1 :/ 2 // Exact division using custom operators
val y = a :+ b // Safe addition
val z = a :* b // Safe multiplicationWhen to use Expression vs Eager:
-
Use
Expressionwhen you want to:- Defer evaluation
- Maintain symbolic form (e.g.,
Ο/2instead of1.5707...) - Allow for algebraic simplification
-
Use
Eagerwhen you:- Need an immediate result
- Are doing simple arithmetic
- Want type-safe numeric operations
Example:
// Lazy - maintains exactness through simplification
val expr: Expression = (β3 + 1) * (β3 - 1) // Simplifies to exactly 2
// Eager - evaluates immediately
val eager: Eager = math"2 * 3" // Evaluates to 6-
normalize(): Converts to canonical symbolic form (stays exact)- Example:
Expressionβ simplifiedExpression
- Example:
-
materialize(): Converts to approximate numeric value (may lose precision)- Example:
ExpressionβDoubleorFuzzy
- Example:
- Follow standard Scala 3 conventions
- Use meaningful variable names
- Add ScalaDoc comments for public APIs
- Keep functions focused and testable
- Prefer immutability
The project uses a standardized set of TODO-style comments to track technical debt and ideas. Configure your IDE to recognize these:
- FIXME: Will be annotated by an Issue #. Top priority to be fixed.
- TESTME: Indicates that there are no unit tests for this code.
- Not used in private methods, or
unapplymethods. Not used for cases designed to catch all possibilities.
- Not used in private methods, or
- TODO: Technical debt to be paid back.
- CONSIDER: An idea for an alternative approach.
- NOTE: A note to readers that might perhaps be a surprise.
- XXX: Just a general comment.
When adding code, use these tags appropriately to communicate intent and priority to other contributors.
Number has comprehensive testing conventions. Please see:
- Testing Conventions - Comprehensive guidelines
- Testing Quick Reference - Quick lookup
# Run all tests
sbt test
# Run tests for a specific module
sbt algebra/test
# Run a specific test class
sbt "testOnly com.phasmidsoftware.number.algebra.ComplexSpec"
# Run tests matching a pattern
sbt "testOnly *Complex*"When contributing code:
-
Write tests for new functionality
- Unit tests for individual methods
- Property-based tests for algebraic laws (when applicable)
- Edge case coverage
-
Maintain existing test coverage
- All existing tests must pass
- Don't reduce coverage without good reason
-
Follow naming conventions
- Test classes end with
Spec - Test methods describe behavior:
"should return exact result for rational division"
- Test classes end with
-
Use appropriate matchers
- For exact equality:
shouldBe,=== - For fuzzy equality:
~==(with appropriate tolerance)
- For exact equality:
It can be useful to debug the behavior of the expression matchers by setting
the implicit logger to use either LogInfo or LogDebug.
The former will only log the results of successful matches, while the latter will
also log all the start of each match attempt.
To do this you can add:
implicit val logger: MatchLogger = MatchLogger(LogDebug, classOf[Expression])in the Expression companion object.
You will also need to make sure that logging is configured for "DEBUG" ("INFO" is sufficient if you specified
LogInfo).
In test/resources/logback.xml, make sure that the root logger is set accordingly.
If expressions aren't simplifying as expected:
-
Check the simplification pipeline phases:
- Components
- Structural
- Identities
- Exact
- Constant
-
Use
.toStringor.renderto see the current form -
Check if the expression has been normalized:
val normalized = expr.normalize() println(normalized.render)
When working with factors (roots, logarithms, etc.):
val num = Number(2, Root(2)) // β2
println(num.factor) // See the factor
println(num.value) // See the underlying value- Check existing issues - Look for related issues or discussions
- Open an issue (for major changes) - Discuss your approach before investing time
- Fork the repository - Create your own fork to work in
-
Create a feature branch:
git checkout -b feature/your-feature-name
-
Make your changes:
- Follow coding conventions
- Write tests
- Update documentation if needed
-
Run tests locally:
sbt test -
Commit with clear messages:
git commit -m "Add support for hyperbolic functions"
-
Push to your fork:
git push origin feature/your-feature-name
-
Open a Pull Request on GitHub:
- Describe what changed and why
- Reference any related issues
- Note any breaking changes
-
Respond to feedback:
- Address review comments
- Update tests if requested
- Keep the PR focused and manageable
- PRs are reviewed by project maintainers
- Tests must pass (CircleCI will run automatically)
- Code quality is checked by Codacy
- At least one approval is required
- Maintainers may request changes or suggest improvements
When adding new features:
- Add ScalaDoc comments for public APIs
- Update relevant markdown docs in the
docs/folder - Add examples if introducing new concepts
- Update README.md if changing core functionality
- GitHub Issues - For bugs or feature requests
- GitHub Discussions - For questions and general discussion
- README.md - For project overview and quick start
By contributing to Number, you agree that your contributions will be licensed under the same license as the project (MIT License).
Thank you for contributing to Number! Your efforts help make exact arithmetic in Scala more accessible and reliable for everyone.