From 330869f3642719e653b54f8e2fded6e4cb5af6b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santtu=20S=C3=B6derholm?= Date: Fri, 29 Aug 2025 12:17:13 +0300 Subject: [PATCH 01/14] Define unit(::Any)m unit(::Funciton) and add a type restriction to unit(::Type{Union{Missing,T}}) This is to resolve errors with uncluding units into FiniteDifferences.jl. See https://github.com/JuliaDiff/FiniteDifferences.jl/pull/244 for details. Tests for Units.jl need to be updated, if this is to be accepted. The totalness of unit introduced via unit(::Ay) might be a bit controversial, but I don't think that should be a problem. In general, things don't have units. Only Numbers do. Closes #806. --- src/utils.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 5adb9356..fdf909a4 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -139,9 +139,11 @@ julia> unit(1.0) == NoUnits true ``` """ +@inline unit(x::Any) = NoUnits # In general, things do not have units. @inline unit(x::Number) = NoUnits @inline unit(x::Type{T}) where {T <: Number} = NoUnits -@inline unit(x::Type{Union{Missing, T}}) where T = unit(T) +@inline unit(x::Function) = NoUnits # Functions do not have units. Function values might. +@inline unit(x::Type{Union{Missing, T}}) where { T <: Number } = unit(T) # Type restriction required here for T, or might result in infinite recursion. @inline unit(x::Type{Missing}) = missing @inline unit(x::Missing) = missing From 51e81a50cfa742bf39721be08f147df913b29ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santtu=20S=C3=B6derholm?= Date: Sat, 30 Aug 2025 13:19:46 +0300 Subject: [PATCH 02/14] test/dates: assume NoUnits instead of MethodErrors in the case of Date types with ambiguous units For example, Years are not always the same length, so they do not have unambiguous units. Many abstract Date types also do not have units. --- test/dates.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/dates.jl b/test/dates.jl index a38f1e76..82ebef85 100644 --- a/test/dates.jl +++ b/test/dates.jl @@ -12,14 +12,14 @@ @test_throws MethodError dimension(T(1)) @test_throws MethodError Unitful.numtype(T) @test_throws MethodError Unitful.numtype(T(1)) - @test_throws MethodError unit(T) - @test_throws MethodError unit(T(1)) + @test unit(T) == NoUnits + @test unit(T(1)) == NoUnits end for p = (CompoundPeriod, CompoundPeriod(), CompoundPeriod(Day(1)), CompoundPeriod(Day(1), Hour(-1))) @test dimension(p) === 𝐓 @test_throws MethodError Unitful.numtype(p) - @test_throws MethodError unit(p) + @test unit(p) == NoUnits end end From 4f2bf7fad749d112f0243da63ce64c548bcc1ec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santtu=20S=C3=B6derholm?= Date: Mon, 1 Sep 2025 13:48:11 +0300 Subject: [PATCH 03/14] Loosen the type restrictions of unit(::Type{Missing,T}) As suggested by @sostock, do not only restrict the type of T to Number, but include FixedPeriod as well, to accommodate Dates-soecific code. Co-authored-by: Sebastian Stock <42280794+sostock@users.noreply.github.com> --- src/utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index fdf909a4..6a256c9e 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -143,7 +143,7 @@ true @inline unit(x::Number) = NoUnits @inline unit(x::Type{T}) where {T <: Number} = NoUnits @inline unit(x::Function) = NoUnits # Functions do not have units. Function values might. -@inline unit(x::Type{Union{Missing, T}}) where { T <: Number } = unit(T) # Type restriction required here for T, or might result in infinite recursion. +@inline unit(x::Type{Union{Missing, T}}) where {T <: Union{Number, FixedPeriod}} = unit(T) # Type restriction required here for T, or might result in infinite recursion. @inline unit(x::Type{Missing}) = missing @inline unit(x::Missing) = missing From c230a6a8be190f4b3db11b93557aad99827afff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santtu=20S=C3=B6derholm?= Date: Mon, 1 Sep 2025 13:57:23 +0300 Subject: [PATCH 04/14] Fully qualify Dates.FixedPeriod in type restriction of method unit(x::Type{Union{Missing, T}}) and remove unwanted unit methods Now the tests run again. Still need to revert the tests that were converted from throwing MethodError to outputting NoUnits. --- src/utils.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 6a256c9e..ec72f4d8 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -139,11 +139,9 @@ julia> unit(1.0) == NoUnits true ``` """ -@inline unit(x::Any) = NoUnits # In general, things do not have units. @inline unit(x::Number) = NoUnits @inline unit(x::Type{T}) where {T <: Number} = NoUnits -@inline unit(x::Function) = NoUnits # Functions do not have units. Function values might. -@inline unit(x::Type{Union{Missing, T}}) where {T <: Union{Number, FixedPeriod}} = unit(T) # Type restriction required here for T, or might result in infinite recursion. +@inline unit(x::Type{Union{Missing, T}}) where {T <: Union{Number, Dates.FixedPeriod}} = unit(T) # Type restriction required here for T, or might result in infinite recursion. @inline unit(x::Type{Missing}) = missing @inline unit(x::Missing) = missing From fe1e2255e35741fc31b8bea21b39b33c11374474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santtu=20S=C3=B6derholm?= Date: Mon, 1 Sep 2025 14:04:48 +0300 Subject: [PATCH 05/14] Revert the Dates-related tests to throw MethodErrors instead of testing against NoUnits This was to accommodate the removed unwanted methods unit(::Any) and unit(::Function). --- test/dates.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/dates.jl b/test/dates.jl index 82ebef85..a38f1e76 100644 --- a/test/dates.jl +++ b/test/dates.jl @@ -12,14 +12,14 @@ @test_throws MethodError dimension(T(1)) @test_throws MethodError Unitful.numtype(T) @test_throws MethodError Unitful.numtype(T(1)) - @test unit(T) == NoUnits - @test unit(T(1)) == NoUnits + @test_throws MethodError unit(T) + @test_throws MethodError unit(T(1)) end for p = (CompoundPeriod, CompoundPeriod(), CompoundPeriod(Day(1)), CompoundPeriod(Day(1), Hour(-1))) @test dimension(p) === 𝐓 @test_throws MethodError Unitful.numtype(p) - @test unit(p) == NoUnits + @test_throws MethodError unit(p) end end From 156ffe2f0eb1cdc6c0d06d0d132a64f8dba42f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santtu=20S=C3=B6derholm?= Date: Wed, 3 Sep 2025 11:21:37 +0300 Subject: [PATCH 06/14] If a unit call bubbles up to unit(::Any), throw a DomainError This allows us to remove the type restriction on T for unit(::Type{Union{Missing,T}}), making it possible for users to define unit on their own types, while at the same time giving a more descriptive error message than StackOverflowError. --- src/utils.jl | 15 ++++++++++++++- test/dates.jl | 6 +++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index ec72f4d8..4cdd70ba 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -139,9 +139,22 @@ julia> unit(1.0) == NoUnits true ``` """ + +""" +This is shown if a user has not defined units for their quantity type. +""" +const MISSING_UNIT_DEFINITION_MESSAGE = + """ + You tried calling unit on an object that has none defined. + Not everything has (or should have) units. + If your type should have units, define a method of unit on them. + """ + +@inline unit(x::Any) = throw( DomainError( x, MISSING_UNIT_DEFINITION_MESSAGE ) ) +@inline unit(x::Type{Any}) = throw( DomainError( x, MISSING_UNIT_DEFINITION_MESSAGE ) ) @inline unit(x::Number) = NoUnits @inline unit(x::Type{T}) where {T <: Number} = NoUnits -@inline unit(x::Type{Union{Missing, T}}) where {T <: Union{Number, Dates.FixedPeriod}} = unit(T) # Type restriction required here for T, or might result in infinite recursion. +@inline unit(x::Type{Union{Missing, T}}) where T = unit(T) # Type restriction required here for T, or might result in infinite recursion. @inline unit(x::Type{Missing}) = missing @inline unit(x::Missing) = missing diff --git a/test/dates.jl b/test/dates.jl index a38f1e76..8146da29 100644 --- a/test/dates.jl +++ b/test/dates.jl @@ -12,14 +12,14 @@ @test_throws MethodError dimension(T(1)) @test_throws MethodError Unitful.numtype(T) @test_throws MethodError Unitful.numtype(T(1)) - @test_throws MethodError unit(T) - @test_throws MethodError unit(T(1)) + @test_throws DomainError unit(T) + @test_throws DomainError unit(T(1)) end for p = (CompoundPeriod, CompoundPeriod(), CompoundPeriod(Day(1)), CompoundPeriod(Day(1), Hour(-1))) @test dimension(p) === 𝐓 @test_throws MethodError Unitful.numtype(p) - @test_throws MethodError unit(p) + @test_throws DomainError unit(p) end end From b43536b99349abc384b1994f9407fd5d3397f49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santtu=20S=C3=B6derholm?= Date: Wed, 3 Sep 2025 21:19:39 +0300 Subject: [PATCH 07/14] Remove now-unnecessary comment about type restriction from unit(::Type{Union{Missing,T}}) Not needed after re-removal of type constraint. Co-authored-by: Sebastian Stock <42280794+sostock@users.noreply.github.com> --- src/utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 4cdd70ba..2e75acd0 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -154,7 +154,7 @@ const MISSING_UNIT_DEFINITION_MESSAGE = @inline unit(x::Type{Any}) = throw( DomainError( x, MISSING_UNIT_DEFINITION_MESSAGE ) ) @inline unit(x::Number) = NoUnits @inline unit(x::Type{T}) where {T <: Number} = NoUnits -@inline unit(x::Type{Union{Missing, T}}) where T = unit(T) # Type restriction required here for T, or might result in infinite recursion. +@inline unit(x::Type{Union{Missing, T}}) where T = unit(T) @inline unit(x::Type{Missing}) = missing @inline unit(x::Missing) = missing From e071b61f9d2f6f2cb0f3133a2d907346c3ddc11f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santtu=20S=C3=B6derholm?= Date: Wed, 3 Sep 2025 21:31:49 +0300 Subject: [PATCH 08/14] Remove method unit(::Any), and move unit(::Type{Any}) for docstring purposes Also changed the exception type thrown by unit(::Type{Any}) to ArgumentError, and changed the error description to be more helpful. --- src/utils.jl | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 2e75acd0..95ee2eb5 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -139,25 +139,25 @@ julia> unit(1.0) == NoUnits true ``` """ - -""" -This is shown if a user has not defined units for their quantity type. -""" -const MISSING_UNIT_DEFINITION_MESSAGE = - """ - You tried calling unit on an object that has none defined. - Not everything has (or should have) units. - If your type should have units, define a method of unit on them. - """ - -@inline unit(x::Any) = throw( DomainError( x, MISSING_UNIT_DEFINITION_MESSAGE ) ) -@inline unit(x::Type{Any}) = throw( DomainError( x, MISSING_UNIT_DEFINITION_MESSAGE ) ) @inline unit(x::Number) = NoUnits @inline unit(x::Type{T}) where {T <: Number} = NoUnits @inline unit(x::Type{Union{Missing, T}}) where T = unit(T) @inline unit(x::Type{Missing}) = missing @inline unit(x::Missing) = missing +""" + unit(::Type{Any}) + +This method is here to prevent infinite recursion and to provide a helpful error message in case it occurs. +""" +@inline unit(x::Type{Any}) = throw( ArgumentError( + """ + The method unit(Any) was called, but this method has no meaningful result. + The call may be due to calling unit(eltype(…)) on a container, + since eltype has a generic fallback method that returns Any. + """ +) ) + """ absoluteunit(::Units) absoluteunit(::Quantity) From 108bc4e5b388fe3c3d402c5abfb1ec7151e7d07c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santtu=20S=C3=B6derholm?= Date: Wed, 3 Sep 2025 21:37:07 +0300 Subject: [PATCH 09/14] Change Dates-related tests to expect MethodErrors again, after the removal of unit(::Any) --- test/dates.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/dates.jl b/test/dates.jl index 8146da29..a38f1e76 100644 --- a/test/dates.jl +++ b/test/dates.jl @@ -12,14 +12,14 @@ @test_throws MethodError dimension(T(1)) @test_throws MethodError Unitful.numtype(T) @test_throws MethodError Unitful.numtype(T(1)) - @test_throws DomainError unit(T) - @test_throws DomainError unit(T(1)) + @test_throws MethodError unit(T) + @test_throws MethodError unit(T(1)) end for p = (CompoundPeriod, CompoundPeriod(), CompoundPeriod(Day(1)), CompoundPeriod(Day(1), Hour(-1))) @test dimension(p) === 𝐓 @test_throws MethodError Unitful.numtype(p) - @test_throws DomainError unit(p) + @test_throws MethodError unit(p) end end From 16c7404152f201e2b10e663fe2a38731683827cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santtu=20S=C3=B6derholm?= Date: Fri, 5 Sep 2025 15:57:32 +0300 Subject: [PATCH 10/14] Shorten error message thrown by unit(Any) Co-authored-by: Sebastian Stock <42280794+sostock@users.noreply.github.com> --- src/utils.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 95ee2eb5..5c2322ef 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -152,9 +152,9 @@ This method is here to prevent infinite recursion and to provide a helpful error """ @inline unit(x::Type{Any}) = throw( ArgumentError( """ - The method unit(Any) was called, but this method has no meaningful result. - The call may be due to calling unit(eltype(…)) on a container, - since eltype has a generic fallback method that returns Any. + unit(Any) was called, which has no meaningful result. \ + This may be due to calling unit(eltype(…)), \ + since eltype has a generic fallback method that returns Any.\ """ ) ) From 63203320d1cc9983b020448e65ab689905d4e182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santtu=20S=C3=B6derholm?= Date: Fri, 5 Sep 2025 16:00:44 +0300 Subject: [PATCH 11/14] Replace docstring of unit(::Type{Any}) with a simple comment The docstring is not needed, as this is not a feature that users need to know about in the documentation. --- src/utils.jl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 5c2322ef..ebd858f3 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -145,11 +145,8 @@ true @inline unit(x::Type{Missing}) = missing @inline unit(x::Missing) = missing -""" - unit(::Type{Any}) +# Prevent infinite recursion, in case unit(Any) is called. -This method is here to prevent infinite recursion and to provide a helpful error message in case it occurs. -""" @inline unit(x::Type{Any}) = throw( ArgumentError( """ unit(Any) was called, which has no meaningful result. \ From 6af1980b46be80f57569e70b1ed1cf821c754e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santtu=20S=C3=B6derholm?= Date: Wed, 10 Sep 2025 10:44:55 +0300 Subject: [PATCH 12/14] Take Julia 1.6 compatibility into account with the unit(Any) error message Line break escaping not supported in multi-line strings in older Julia versions. Co-authored-by: Sebastian Stock <42280794+sostock@users.noreply.github.com> --- src/utils.jl | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index ebd858f3..6cbb81d2 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -148,11 +148,7 @@ true # Prevent infinite recursion, in case unit(Any) is called. @inline unit(x::Type{Any}) = throw( ArgumentError( - """ - unit(Any) was called, which has no meaningful result. \ - This may be due to calling unit(eltype(…)), \ - since eltype has a generic fallback method that returns Any.\ - """ + "unit(Any) was called, which has no meaningful result. This may be due to calling unit(eltype(…)), since eltype has a generic fallback method that returns Any." ) ) """ From 7ea4b1fd3a6da7d55622df69f60f2284db317692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santtu=20S=C3=B6derholm?= Date: Mon, 15 Sep 2025 11:05:36 +0300 Subject: [PATCH 13/14] Follow code style of project No empty lines between comments and code, and no spaces after opening parentheses of function invocations. Co-authored-by: Sebastian Stock <42280794+sostock@users.noreply.github.com> --- src/utils.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 6cbb81d2..b6e735dc 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -146,8 +146,7 @@ true @inline unit(x::Missing) = missing # Prevent infinite recursion, in case unit(Any) is called. - -@inline unit(x::Type{Any}) = throw( ArgumentError( +@inline unit(x::Type{Any}) = throw(ArgumentError( "unit(Any) was called, which has no meaningful result. This may be due to calling unit(eltype(…)), since eltype has a generic fallback method that returns Any." ) ) From 48b962915c9898d5df40fd0908e71195fa6f5c23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santtu=20S=C3=B6derholm?= Date: Mon, 15 Sep 2025 11:06:36 +0300 Subject: [PATCH 14/14] Further code style changes Remove space between closing parentheses of nested function invocations Co-authored-by: Sebastian Stock <42280794+sostock@users.noreply.github.com> --- src/utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index b6e735dc..23efcfdc 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -148,7 +148,7 @@ true # Prevent infinite recursion, in case unit(Any) is called. @inline unit(x::Type{Any}) = throw(ArgumentError( "unit(Any) was called, which has no meaningful result. This may be due to calling unit(eltype(…)), since eltype has a generic fallback method that returns Any." -) ) +)) """ absoluteunit(::Units)