diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml index 9a8c7086..a6e7a266 100644 --- a/.github/workflows/documentation.yaml +++ b/.github/workflows/documentation.yaml @@ -28,6 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 + - run: sudo apt install -y ghostscript fonts-freefont-ttf - uses: julia-actions/setup-julia@v2 with: version: 1 diff --git a/Project.toml b/Project.toml index 1853beba..c0464535 100644 --- a/Project.toml +++ b/Project.toml @@ -13,12 +13,15 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112" +Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" +LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" [extensions] ConstructionBaseUnitfulExt = "ConstructionBase" ForwardDiffExt = "ForwardDiff" InverseFunctionsUnitfulExt = "InverseFunctions" +LatexifyExt = ["Latexify", "LaTeXStrings"] PrintfExt = "Printf" [compat] @@ -27,6 +30,8 @@ ConstructionBase = "1" Dates = "<0.0.1, 1" ForwardDiff = "0.10, 1" InverseFunctions = "0.1" +LaTeXStrings = "1.2.0" +Latexify = "0.16.8" LinearAlgebra = "<0.0.1, 1" Printf = "<0.0.1, 1" REPL = "<0.0.1, 1" @@ -39,6 +44,8 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112" +Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" +LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" @@ -46,4 +53,4 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Aqua", "ConstructionBase", "ForwardDiff", "InverseFunctions", "LinearAlgebra", "Test", "Random", "REPL", "Printf"] +test = ["Aqua", "ConstructionBase", "ForwardDiff", "InverseFunctions", "Latexify", "LaTeXStrings", "LinearAlgebra", "Test", "Random", "REPL", "Printf"] diff --git a/README.md b/README.md index c1e8ee5f..f4a88517 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ mathematical operations and collections that are found in Julia base. ### Feature additions - [UnitfulEquivalences.jl](https://github.com/sostock/UnitfulEquivalences.jl): Enables conversion between equivalent quantities of different dimensions, e.g. between energy and wavelength of a photon. -- [UnitfulLatexify.jl](https://github.com/gustaphe/UnitfulLatexify.jl): Pretty print units and quantities in LaTeX format. - [UnitfulParsableString.jl](https://github.com/michikawa07/UnitfulParsableString.jl): Add a `Base.string` method that converts quantities and units to parsable strings. - [UnitfulBuckinghamPi.jl](https://github.com/rmsrosa/UnitfulBuckinghamPi.jl): Solves for the adimensional Pi groups in a list of Unitful parameters, according to the Buckingham-Pi Theorem. - [NaturallyUnitful.jl](https://github.com/MasonProtter/NaturallyUnitful.jl): Convert to and from natural units in physics. @@ -38,6 +37,7 @@ mathematical operations and collections that are found in Julia base. - [DimensionfulAngles.jl](https://github.com/cmichelenstrofer/DimensionfulAngles.jl): Adds angle as a dimension. This allows dispatching on angles and derived quantities. - [Dimensionless.jl](https://github.com/martinkosch/Dimensionless.jl): Contains tools to switch between dimensional bases, conduct dimensional analysis and solve similitude problems. - [UnitfulRecipes.jl](https://github.com/jw3126/UnitfulRecipes.jl) (deprecated): Adds automatic labels and supports plot axes with units for [Plots.jl](https://github.com/JuliaPlots/Plots.jl). (UnitfulRecipes.jl is now included in Plots.jl.) +- [UnitfulLatexify.jl](https://github.com/gustaphe/UnitfulLatexify.jl) (deprecated): Pretty print units and quantities in LaTeX format. (This package is now an extension to Unitful, so that loading both Unitful and [Latexify.jl](https://github.com/korsbo/Latexify.jl) loads this functionality.) ## Related packages diff --git a/docs/Project.toml b/docs/Project.toml index 418f2c7b..4e9dd302 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -2,9 +2,13 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112" +LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" +tectonic_jll = "d7dd28d6-a5e6-559c-9131-7eb760cdacc5" [compat] Documenter = "1" -Latexify = "0.16" +Latexify = "0.16.9" diff --git a/docs/generate_latex_images.jl b/docs/generate_latex_images.jl new file mode 100644 index 00000000..e2349db1 --- /dev/null +++ b/docs/generate_latex_images.jl @@ -0,0 +1,147 @@ +using LaTeXStrings, Unitful, Latexify, tectonic_jll + +commands = [ + :(latexify(612.2u"nm")), + :(latexify(u"kg*m/s^2")), + :(latexify(612.2u"nm"; fmt=SiunitxNumberFormatter())), + :(latexify(u"kg*m/s^2"; fmt=SiunitxNumberFormatter())), + :(latexify(612.2u"nm"; fmt=SiunitxNumberFormatter(; simple=true))), + :(latexify(u"kg*m/s^2"; fmt=SiunitxNumberFormatter(; simple=true))), + :(latexify((1, 2, 4) .* u"m"; fmt=SiunitxNumberFormatter())), +] +tab1 = map(commands) do command + LaTeXString.([ + "\\verb+$(string(command))+", + "\\verb+$(eval(command))+", + "$(eval(command)) ", + ]) +end +ltab1 = latextabular(tab1, adjustment=:l, transpose=true, latex=false, booktabs=true, + head=["julia", "\\LaTeX", "Result"]) +# Setting an explicit white background color results in transparent PDF, so go offwhite. +ltab1 = LaTeXString("\\definecolor{offwhite}{rgb}{0.999,0.999,0.999}\n\\pagecolor{offwhite}\n\\color{black}\n" * ltab1) + +render(ltab1, MIME("image/png"); use_tectonic=true, + name=(@__DIR__)*"/src/assets/latex-examples", + packages=["booktabs", "color", "siunitx"], + documentclass=("standalone")) + +functions = [ + x -> "\\verb+$(string(x))+", + x -> latexify(x), + x -> latexify(x; fmt=SiunitxNumberFormatter()), + x -> latexify(x; fmt=SiunitxNumberFormatter(; simple=true)), +] +allunits = begin + uparse.([ + "nH*m/Hz", + "m", + "s", + "A", + "K", + "cd", + "g", + "mol", + "sr", + "rad", + "°", + "Hz", + "N", + "Pa", + "J", + "W", + "C", + "V", + "S", + "F", + "H", + "T", + "Wb", + "lm", + "lx", + "Bq", + "Gy", + "Sv", + "kat", + #"percent", # Messes with comments + # "permille", # Undefined in all formats + # "pertenthousand", # Undefined in all formats (butchered) + "°C", + "°F", # No longer in siunitx + "minute", + "hr", + "d", + "wk", # Undefined in siunitx + "yr", # Undefined in siunitx + "rps", # Undefined in siunitx + "rpm", # Undefined in siunitx + "a", # Undefined in siunitx + "b", + "L", + "M", # Undefined in siunitx + "eV", + "Hz2π", # Butchered by encoding + "bar", + "atm", # Undefined in siunitx + "Torr", # Undefined in siunitx + "c", # Undefined in siunitx + "u", # Undefined in siunitx + "ge", # Undefined in siunitx + "Gal", # Undefined in siunitx + "dyn", # Undefined in siunitx + "erg", # Undefined in siunitx + "Ba", # Undefined in siunitx + "P", # Undefined in siunitx + "St", # Undefined in siunitx + #"Gauss", # errors in testing, maybe from Unitful.jl's dev branch? + #"Oe", # errors in testing, maybe from Unitful.jl's dev branch? + #"Mx", # errors in testing, maybe from Unitful.jl's dev branch? + "inch", # Undefined in siunitx + "mil", # Undefined in siunitx + "ft", # Undefined in siunitx + "yd", # Undefined in siunitx + "mi", # Undefined in siunitx + "angstrom", # Undefined in mathrm,siunitxsimple + "ac", # Undefined in siunitx + "Ra", # Undefined in siunitx + "lb", # Undefined in siunitx + "oz", # Undefined in siunitx + "slug", # Undefined in siunitx + "dr", # Undefined in siunitx + "gr", # Undefined in siunitx + "lbf", # Undefined in siunitx + "cal", # Undefined in siunitx + "btu", # Undefined in siunitx + "psi", # Undefined in siunitx + #"dBHz", # Cannot *yet* be latexified. + #"dBm", # Cannot *yet* be latexified. + #"dBV", # Cannot *yet* be latexified. + #"dBu", # Cannot *yet* be latexified. + #"dBμV", # Cannot *yet* be latexified. + #"dBSPL", # Cannot *yet* be latexified. + #"dBFS", # Cannot *yet* be latexified. + #"dBΩ", # Cannot *yet* be latexified. + #"dBS", # Cannot *yet* be latexified. + ]) +end + +tab2 = map(allunits) do unit + [LaTeXString(f(unit)) for f in functions] +end +ltab2 = latextabular(tab2, adjustment=:l, transpose=true, latex=false, booktabs=true, + head=["Name", "Default number formatter", "\\verb+SiunitxNumberFormatter()+", "\\verb+SiunitxNumberFormatter(;simple=true)+"]) +# Set background to not-quite-white so it doesn't get treated as transparent +ltab2 = LaTeXString( + """ + \\setmainfont{FreeSerif} + \\setmonofont{FreeMono} + \\definecolor{offwhite}{rgb}{0.999,0.999,0.999} + \\pagecolor{offwhite} + \\color{black} + """ * ltab2) + +render(ltab2, MIME("image/png"); use_tectonic=true, + tectonic_flags=`-Z continue-on-errors`, + name=(@__DIR__)*"/src/assets/latex-allunits", + packages=["booktabs", "color", "siunitx", "fontspec"], + documentclass=("standalone")) diff --git a/docs/make.jl b/docs/make.jl index c44a31a1..72827103 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,5 +1,8 @@ using Documenter, Unitful, Dates +@info "Generating latex images for documentation" +include("generate_latex_images.jl") + DocMeta.setdocmeta!(Unitful, :DocTestSetup, :(using Unitful)) makedocs( @@ -19,6 +22,7 @@ makedocs( "Logarithmic scales" => "logarithm.md" "Temperature scales" => "temperature.md" "Interoperability with `Dates`" => "dates.md" + "Latexifying units" => "latexify.md" "Extending Unitful" => "extending.md" "Troubleshooting" => "trouble.md" "Pre-defined units and constants" => "defaultunits.md" diff --git a/docs/src/latexify.md b/docs/src/latexify.md new file mode 100644 index 00000000..a0838d25 --- /dev/null +++ b/docs/src/latexify.md @@ -0,0 +1,177 @@ +# Latexify extension + +Unitful has an extension for [Latexify](https://github.com/korsbo/Latexify.jl), which was formerly implemented as a separate package called UnitfulLatexify.jl. + +The default usage is pretty intuitive: + +```@setup main +using LaTeXStrings # for some manual pretty-printing +``` + +```@example main +using Unitful, Latexify + +a = 9.82u"m/s^2" +t = 4u"s" +x = a*t^2 + +latexify(x) +``` + +or more usefully: + +```@example main +latexify(:(x = a*t^2 = $x)) +``` + +This of course also works for `Units` objects by themselves: + +```@example main +latexify(u"kg*m") +``` + +Some more usage examples: + +![](assets/latex-examples.png) + + +## Arrays + +Because Latexify is recursive, an array of unitful quantities is shown as +expected: + + +```@example main +latexify([12u"m", 1u"m^2", 4u"m^3"]) +LaTeXString("\$\$" * ans * "\$\$") # hide +``` + +A special case is an array where all elements have the same unit, and here +the extension does some extra work: +```@example main +latexify([1, 2, 3]u"cm") +LaTeXString("\$\$" * ans * "\$\$") # hide +``` + + +## siunitx.sty + +If you are exporting your numbers to an actual LaTeX document, you will of +course want to use the commands from `siunitx.sty` rather than the `\mathrm` +style used by default. To this end you can use Latexify's `fmt=SiunitxNumberFormatter` for `\qty{8}{\second\meter\per\kilo\gram}` style and `fmt=SiunitxNumberFormatter(simple=true)` for +`\qty{8}{s.m/kg}`. Like other Latexify keywords, this can be set to be a default +by using `set_default(fmt=SiunitxNumberFormatter())`, or given with each latexification +command: + +```@example main +latexify(612.2u"nm"; fmt=SiunitxNumberFormatter()) # This will not render right without the `siunitx` package +print(ans) # hide +``` + +### Lists + +Another thing that `siunitx` does uniquely is lists and ranges of quantities. +To get `siunitx`'s list behavior, pass a tuple instead of an array; +if you want a tuple to be written as an array instead, use `collect(x)` or `[x...]` to explicitly it into an array first. + +```@example main +latexify((1, 2, 3).*u"m") +print(ans) # hide +``` +```@example main +latexify((1, 2, 3).*u"m"; fmt=SiunitxNumberFormatter()) +print(ans) # hide +``` +```@example main +latexify(collect((1, 2, 3).*u"m"); fmt=SiunitxNumberFormatter()) +print(ans) # hide +``` + + +## Plots labels + +This extension also interfaces with `Plots` by way of implementing a two-argument `(label, unit)` recipe: + +```@example main +latexify("v", u"km/s") +``` + +This enables this dreamlike example: + +```@example plot +using Unitful, Plots, Latexify +gr() +default(fontfamily="Computer Modern") + +m = randn(10)u"kg" +v = randn(10)u"m/s" +plot(m, v; xguide="\\mathrm{mass}", yguide="v_x", unitformat=latexify) +``` + +This format, ``v_x\;\left/\;\mathrm{m}\,\mathrm{s}^{-1}\right.``, is subject to personal +preference. A couple other defaults are provided: +- `:slash`, ``v_x\;\left/\;\mathrm{m}\,\mathrm{s}^{-1}\right.`` +- `:round`, ``v_x\;\left(\mathrm{m}\,\mathrm{s}^{-1}\right)`` +- `:square`, ``v_x\;\left[\mathrm{m}\,\mathrm{s}^{-1}\right]`` +- `:frac`, ``\frac{v_x}{\mathrm{m}\,\mathrm{s}^{-1}}`` + +To use these in a plot call, either pass a function like +``` +(l,u) -> latexify(l, u; labelformat=:slash)` +``` +or call `Latexify.set_default(labelformat=:square)`, then pass `latexify` as your unitformat. + +```@example plot +args = (m, v) +kwargs = (xguide="\\mathrm{mass}", yguide="v_x", legend=false) +Latexify.set_default(labelformat=:square) +plot( + plot(args...; kwargs..., unitformat=(l,u)->latexify(l, u, labelformat=:slash)), + plot(args...; kwargs..., unitformat=(l, u)->latexify(l, u, labelformat=:round)), + plot(args...; kwargs..., unitformat=latexify), + plot(args...; kwargs..., unitformat=(l, u)->latexify(l, u, labelformat=:frac)), + plot(args...; kwargs..., unitformat=(l, u)->string("\$", l, " \\rightarrow ", latexraw(u), "\$")), +) +``` + +## Pluto notebooks +One use case is in Pluto notebooks, where you can +write + +```julia +Markdown.parse(""" +The period is $(@latexrun T = $(2.5u"ms")), so the frequency is $(@latexdefine f = 1/T post=u"kHz"). +""") +``` +which renders as + +> The period is $T = 2.5\;\mathrm{ms}$, so the frequency is $f = \frac{1}{T} = 0.4\;\mathrm{kHz}$. + +Note that the quantity has to be interpolated (put inside a +dollar-parenthesis), or Latexify will interpret it as a multiplication between +a number and a call to `@u_str`. + + +## Per-modes + +In mathrm-mode, one might prefer ``\mathrm{J}\,/\,\mathrm{kg}`` or +``\frac{\mathrm{J}}{\mathrm{kg}}`` over ``\mathrm{J}\,\mathrm{kg}^{-1}``. This +can be achieved by supplying `permode=:slash` or `permode=:frac` respectively, rather than the default `permode=:power`. + +These will have no effect with `SiunitxNumberFormatter`, because the latex package handles +this for you, and you can set it in your document. + +## New siunitx syntax + +The new syntax from `siunitx v3` (`\qty, \unit` rather +than `\SI, \si`) is used by default. If you cannot upgrade `siunitx`, there's the option +to use `fmt=SiunitxNumberFormatter(version=2)`. + +## A more complete list of defined units + +Below is a poorly scraped list of units defined in `Unitful` and what comes out +if you run it through `latexify`. Feel free to create an issue if there's a +unit missing or being incorrectly rendered (and suggest a better ``\LaTeX`` +representation if you know one). + +![](assets/latex-allunits.png) diff --git a/ext/LatexifyExt.jl b/ext/LatexifyExt.jl new file mode 100644 index 00000000..f8eec814 --- /dev/null +++ b/ext/LatexifyExt.jl @@ -0,0 +1,406 @@ +#=========================================# +# Extension for Unitful.jl + Latexify.jl, # +# based on UnitfulLatexify.jl by # +# David Gustavsson (@gustaphe) # +#=========================================# +module LatexifyExt +using Unitful: + Unitful, + Unit, + Units, + AbstractQuantity, + AffineUnits, + AffineQuantity, + power, + abbr, + name, + tens, + sortexp, + unit, + NoDims, + ustrip, + @u_str, + genericunit, + has_unit_spacing +using Latexify: + Latexify, + @latexrecipe, + latexify, + _latexarray, + latexraw, + FancyNumberFormatter, + PlainNumberFormatter, + StyledNumberFormatter, + SiunitxNumberFormatter, + AbstractNumberFormatter +using LaTeXStrings: LaTeXString + +import Latexify: latexify + +import Base.* + +# utility functions ------------------ + +function get_formatter(kwargs) + fmt = get(kwargs, :fmt, FancyNumberFormatter()) + if fmt isa String + fmt = StyledNumberFormatter(fmt) + end + return fmt +end +get_format_env(fmt::SiunitxNumberFormatter) = :raw +get_format_env(fmt) = :inline + + +function getunitname(p::T, unitformat) where {T<:Unit} + unitname = get(unitnames, (unitformat, name(p)), nothing) + isnothing(unitname) || return unitname + if unitformat === :siunitx + return "\\$(lowercase(String(name(p))))" + end + return abbr(p) +end + +function listunits(::T) where {T<:Units} + return sortexp(T.parameters[1]) +end + +""" +```julia +intersperse(t, delim) +``` +Create a vector whose elements alternate between the elements of `t` and `delim`, analogous +to `join` for strings. + +# Example +```julia +julia> intersperse((1, 2, 3, 4), :a) +[1, :a, 2, :a, 3, :a, 4] +``` +""" +function intersperse(t::T, delim::U) where {T,U} + iszero(length(t)) && return () + L = length(t) * 2 - 1 + out = Vector{Union{typeof.(t)...,U}}(undef, L) + out[1:2:L] .= t + out[2:2:L] .= delim + return out +end + +# default ------------------------------ + +@latexrecipe function f(p::Unit) + fmt = get_formatter(kwargs) + env --> get_format_env(fmt) + return _transform(p, fmt) +end + +@latexrecipe function f(u::Units; permode=:power) + fmt = get_formatter(kwargs) + env --> get_format_env(fmt) + return _transform(u, fmt) +end + +@latexrecipe function f(q::AbstractQuantity) + fmt = get_formatter(kwargs) + env --> get_format_env(fmt) + operation := :* + + return _transform(q, fmt) +end + +struct NakedUnits + u::Units +end +struct NakedNumber + n::Number +end + +@latexrecipe function f(u::NakedUnits; permode=:power) + fmt = get_formatter(kwargs) + unitlist = listunits(u.u) + if fmt isa SiunitxNumberFormatter + fmt.simple && return Expr(:latexifymerge, intersperse(unitlist, ".")...) + return Expr(:latexifymerge, unitlist...) + end + if permode === :power + return Expr(:latexifymerge, intersperse(unitlist, "\\,")...) + end + + numunits = [x for x in unitlist if power(x) >= 0] + denunits = [typeof(x)(tens(x), -power(x)) for x in unitlist if power(x) < 0] + + numerator = intersperse(numunits, "\\,") + if isempty(denunits) + return Expr(:latexifymerge, numerator...) + end + if isempty(numunits) + numerator = [1] + end + denominator = intersperse(denunits, "\\,") + + if permode === :slash + return Expr(:latexifymerge, numerator..., "\\,/\\,", denominator...) + end + if permode === :frac + return Expr(:latexifymerge, "\\frac{", numerator..., "}{", denominator..., "}") + end + return error("permode $permode undefined.") +end + +@latexrecipe function f(n::NakedNumber) + fmt = get_formatter(kwargs) + if fmt isa SiunitxNumberFormatter + fmt := PlainNumberFormatter() + end + return n.n +end + +function _transform(p::Unit, fmt::SiunitxNumberFormatter) + unitformat = fmt.simple ? :siunitxsimple : :siunitx + prefix = prefixes[(unitformat, tens(p))] + pow = power(p) + unitname = getunitname(p, unitformat) + if fmt.simple + per = "" + expo = pow == 1//1 ? "" : "^{$(latexify(pow; fmt="%g", env=:raw))}" + else + per = pow < 0 ? "\\per" : "" + pow = abs(pow) + expo = pow == 1//1 ? "" : "\\tothe{$(latexify(pow; fmt="%g", env=:raw))}" + end + return LaTeXString("$per$prefix$unitname$expo") +end +function _transform(p::Unit, fmt::AbstractNumberFormatter) + prefix = prefixes[(:mathrm, tens(p))] + unitname = getunitname(p, :mathrm) + pow = power(p) + expo = pow == 1//1 ? "" : "^{$(latexify(pow; fmt="%g", env=:raw))}" + return LaTeXString("\\mathrm{$prefix$unitname}$expo") +end + +function _transform(u::Units, fmt::SiunitxNumberFormatter) + opening = fmt.version < 3 ? "\\si{" : "\\unit{" + return Expr(:latexifymerge, opening, NakedUnits(u), "}") +end +_transform(u::Units, ::AbstractNumberFormatter) = Expr(:latexifymerge, NakedUnits(u)) + +function _transform(q::AbstractQuantity, fmt::SiunitxNumberFormatter) + opening = fmt.version < 3 ? "\\SI{" : "\\qty{" + return Expr(:latexifymerge, opening, NakedNumber(q.val), "}{", NakedUnits(unit(q)), "}") +end +function _transform(q::AbstractQuantity, ::AbstractNumberFormatter) + Expr( + :latexifymerge, + NakedNumber(q.val), + has_unit_spacing(unit(q)) ? "\\;" : nothing, + NakedUnits(unit(q)), + ) +end +_transform(n::NakedNumber, ::SiunitxNumberFormatter) = PlainNumberFormatter(n.n) + + +# affine ------------------------------- + +@latexrecipe function f(u::AffineUnits) + fmt = get_formatter(kwargs) + if u == Unitful.°C + unitname = :Celsius + elseif u == Unitful.°F + unitname = :Fahrenheit + else + # If it's not celsius or farenheit, let it do the default thing + return genericunit(u) + end + if fmt isa SiunitxNumberFormatter + env --> :raw + return Expr(:latexifymerge, "\\unit{", unitnames[(:siunitx, unitname)], "}") + end + env --> :inline + return LaTeXString(unitnames[(:mathrm, unitname)]) +end + +@latexrecipe function f(q::AffineQuantity) + fmt = get_formatter(kwargs) + u = unit(q) + if u == Unitful.°C + unitname = :Celsius + elseif u == Unitful.°F + unitname = :Fahrenheit + else + # If it's not celsius or farenheit, let it do the default thing + return ustrip(q)*genericunit(u) + end + if fmt isa SiunitxNumberFormatter + env --> :raw + return Expr( + :latexifymerge, + "\\qty{", + NakedNumber(q.val), + "}{", + unitnames[(:siunitx, unitname)], + "}", + ) + end + env --> :inline + return Expr(:latexifymerge, q.val, "\\;\\mathrm{", unitnames[(:mathrm, unitname)], "}") +end + +# arrays ------------------------- +@latexrecipe function f( # Array{Quantity{U}} + a::AbstractArray{<:AbstractQuantity{N,D,U}}; +) where {N<:Number,D,U} + # Array of quantities with the same unit + env --> :equation + return Expr( + :latexifymerge, ustrip.(a), has_unit_spacing(first(a)) ? "\\;" : "", unit(first(a)) + ) +end + +@latexrecipe function f( # Tuple{Quantity{U}} + l::Tuple{T,Vararg{T}}, +) where {T<:AbstractQuantity{N,D,U}} where {N<:Number,D,U} + fmt = get_formatter(kwargs) + if fmt isa SiunitxNumberFormatter + env --> :raw + opening = fmt.version < 3 ? "\\SIlist{" : "\\qtylist{" + return Expr( + :latexifymerge, + opening, + intersperse(NakedNumber.(ustrip.(l)), ";")..., + "}{", + NakedUnits(unit(first(l))), + "}", + ) + end + return collect(l) +end + +# label (for plots) ------------------------------------ + +@latexrecipe function f(l::AbstractString, u::Units; labelformat=:slash) + labelformat === :slash && return Expr(:latexifymerge, l, "\\;\\left/\\;", u, "\\right.") + labelformat === :square && return Expr(:latexifymerge, l, "\\;\\left[", u, "\\right]") + labelformat === :round && return Expr(:latexifymerge, l, "\\;\\left(", u, "\\right)") + labelformat === :frac && return Expr(:latexifymerge, "\\frac{", l, "}{", u, "}") + error("Unknown labelformat $labelformat") +end + +# prefixes ------------------------------ + +""" +prefixes are listed in this dictionary +`(unitformat::Symbol, pow::Integer) => prefix::String` +""" +const prefixes = begin + Dict( + (:mathrm, -24) => "y", + (:mathrm, -21) => "z", + (:mathrm, -18) => "a", + (:mathrm, -15) => "f", + (:mathrm, -12) => "p", + (:mathrm, -9) => "n", + (:mathrm, -6) => "\\mu{}", + (:mathrm, -3) => "m", + (:mathrm, -2) => "c", + (:mathrm, -1) => "d", + (:mathrm, 0) => "", + (:mathrm, 1) => "D", + (:mathrm, 2) => "h", + (:mathrm, 3) => "k", + (:mathrm, 6) => "M", + (:mathrm, 9) => "G", + (:mathrm, 12) => "T", + (:mathrm, 15) => "P", + (:mathrm, 18) => "E", + (:mathrm, 21) => "Z", + (:mathrm, 24) => "Y", + (:siunitx, -24) => "\\yocto", + (:siunitx, -21) => "\\zepto", + (:siunitx, -18) => "\\atto", + (:siunitx, -15) => "\\femto", + (:siunitx, -12) => "\\pico", + (:siunitx, -9) => "\\nano", + (:siunitx, -6) => "\\micro", + (:siunitx, -3) => "\\milli", + (:siunitx, -2) => "\\centi", + (:siunitx, -1) => "\\deci", + (:siunitx, 0) => "", + (:siunitx, 1) => "\\deka", + (:siunitx, 2) => "\\hecto", + (:siunitx, 3) => "\\kilo", + (:siunitx, 6) => "\\mega", + (:siunitx, 9) => "\\giga", + (:siunitx, 12) => "\\tera", + (:siunitx, 15) => "\\peta", + (:siunitx, 18) => "\\exa", + (:siunitx, 21) => "\\zetta", + (:siunitx, 24) => "\\yotta", + (:siunitxsimple, -24) => "y", + (:siunitxsimple, -21) => "z", + (:siunitxsimple, -18) => "a", + (:siunitxsimple, -15) => "f", + (:siunitxsimple, -12) => "p", + (:siunitxsimple, -9) => "n", + (:siunitxsimple, -6) => "\\u", + (:siunitxsimple, -3) => "m", + (:siunitxsimple, -2) => "c", + (:siunitxsimple, -1) => "d", + (:siunitxsimple, 0) => "", + (:siunitxsimple, 1) => "D", + (:siunitxsimple, 2) => "h", + (:siunitxsimple, 3) => "k", + (:siunitxsimple, 6) => "M", + (:siunitxsimple, 9) => "G", + (:siunitxsimple, 12) => "T", + (:siunitxsimple, 15) => "P", + (:siunitxsimple, 18) => "E", + (:siunitxsimple, 21) => "Z", + (:siunitxsimple, 24) => "Y", + ) +end + +# unit names ------------------------------ + +"""" +`unitnames` + +Unit names generally follow a simple scheme, but there are exceptions, listed in this +dictionary: `(unitformat::Symbol, name::Symbol) => unitname::String` +""" +const unitnames = begin + Dict( + (:mathrm, :Percent) => "\\%", + (:siunitxsimple, :Percent) => "\\%", + (:mathrm, :Degree) => "^{\\circ}", + (:siunitxsimple, :Degree) => "\\degree", + (:siunitx, :eV) => "\\electronvolt", + (:mathrm, :Ohm) => "\\Omega", + (:mathrm, :Celsius) => "^\\circ C", + (:siunitx, :Celsius) => "\\celsius", + (:siunitxsimple, :Celsius) => "\\celsius", + (:mathrm, :Fahrenheit) => "^\\circ F", + (:siunitx, :Fahrenheit) => "\\fahrenheit", + (:siunitxsimple, :Fahrenheit) => "\\fahrenheit", + (:siunitxsimple, :Angstrom) => "\\angstrom", + (:mathrm, :Angstrom) => "\\AA", + (:mathrm, :DoubleTurn) => "\\S", + (:mathrm, :Turn) => "\\tau", + (:mathrm, :HalfTurn) => "\\pi", + (:mathrm, :Quadrant) => "\\frac{\\pi}{2}", + (:mathrm, :Sextant) => "\\frac{\\pi}{3}", + (:mathrm, :Octant) => "\\frac{\\pi}{4}", + (:mathrm, :ClockPosition) => "\\frac{\\pi}{12}", + (:mathrm, :HourAngle) => "\\frac{\\pi}{24}", + (:mathrm, :CompassPoint) => "\\frac{\\pi}{32}", + (:mathrm, :Hexacontade) => "\\frac{\\pi}{60}", + (:mathrm, :BinaryRadian) => "\\frac{\\pi}{256}", + (:mathrm, :DiameterPart) => "\\oslash", # This is slightly wrong + (:mathrm, :Gradian) => "^g", + (:mathrm, :Arcminute) => "'", + (:mathrm, :Arcsecond) => "''", + (:mathrm, :ArcsecondShort) => "''", + ) +end + +end # LatexifyExt \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 6fe0033d..c0e560f3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,6 +4,8 @@ import Unitful: DimensionError, AffineError import Unitful: LogScaled, LogInfo, Level, Gain, MixedUnits, Decibel import Unitful: FreeUnits, ContextUnits, FixedUnits, AffineUnits, AffineQuantity import ForwardDiff +import Latexify: Latexify, latexify, @latexify, FancyNumberFormatter, SiunitxNumberFormatter +import LaTeXStrings: LaTeXString, @L_str import Unitful: nm, μm, mm, cm, m, km, inch, ft, mi, @@ -1771,6 +1773,137 @@ VERSION ≥ v"1.9.0" && @testset "printf" begin @test (@sprintf "%f %d %.2f %05d" 1.23u"m" 123.4u"°" 0.1234u"W" 12.34u"km") == "1.230000 m 123° 0.12 W 00012 km" end +isdefined(Base, :get_extension) && @testset "Latexify extension" begin + +function unitfullatexifytest(val, mathrmexpected, siunitxexpected, siunitxsimpleexpected) + @test latexify(val; fmt=FancyNumberFormatter()) == + LaTeXString(replace(mathrmexpected, "\r\n" => "\n")) + @test latexify(val; fmt=SiunitxNumberFormatter()) == + LaTeXString(replace(siunitxexpected, "\r\n" => "\n")) + @test latexify(val; fmt=SiunitxNumberFormatter(simple=true)) == + LaTeXString(replace(siunitxsimpleexpected, "\r\n"=>"\n")) +end + +@testset "Latexify units" begin + unitfullatexifytest( + u"H*J/kg", + raw"$\mathrm{H}\,\mathrm{J}\,\mathrm{kg}^{-1}$", + raw"\unit{\henry\joule\per\kilo\gram}", + raw"\unit{H.J.kg^{-1}}", + ) + unitfullatexifytest( + 24.7e9u"Gm/s^2", + raw"$2.47 \cdot 10^{10}\;\mathrm{Gm}\,\mathrm{s}^{-2}$", + raw"\qty{2.47e10}{\giga\meter\per\second\tothe{2}}", + raw"\qty{2.47e10}{Gm.s^{-2}}", + ) + unitfullatexifytest( + u"percent", raw"$\mathrm{\%}$", raw"\unit{\percent}", raw"\unit{\%}" + ) + unitfullatexifytest( + 2u"°C", raw"$2\;\mathrm{^\circ C}$", raw"\qty{2}{\celsius}", raw"\qty{2}{\celsius}" + ) + unitfullatexifytest( + 1u"°", raw"$1\mathrm{^{\circ}}$", raw"\qty{1}{\degree}", raw"\qty{1}{\degree}" + ) + unitfullatexifytest( + [1, 2, 3]*m, + raw""" + \begin{equation} + \left[ + \begin{array}{c} + 1 \\ + 2 \\ + 3 \\ + \end{array} + \right]\;\mathrm{m} + \end{equation} + """, + raw""" + \begin{equation} + \left[ + \begin{array}{c} + \num{1} \\ + \num{2} \\ + \num{3} \\ + \end{array} + \right]\;\unit{\meter} + \end{equation} + """, + raw""" + \begin{equation} + \left[ + \begin{array}{c} + \num{1} \\ + \num{2} \\ + \num{3} \\ + \end{array} + \right]\;\unit{m} + \end{equation} + """, + ) + unitfullatexifytest((1,2,3).*m, + raw""" + \begin{equation} + \left[ + \begin{array}{c} + 1 \\ + 2 \\ + 3 \\ + \end{array} + \right]\;\mathrm{m} + \end{equation} + """, + raw"\qtylist{1;2;3}{\meter}", + raw"\qtylist{1;2;3}{m}", + ) + + @test latexify(24.7e9u"Gm/s^2"; fmt="%.1e") == + L"$2.5e+10\;\mathrm{Gm}\,\mathrm{s}^{-2}$" + @test latexify(5.9722e24u"kg"; fmt=SiunitxNumberFormatter(version=2)) == + raw"\SI{5.9722e24}{\kilo\gram}" + @test latexify(u"eV"; fmt=SiunitxNumberFormatter(version=2)) == raw"\si{\electronvolt}" +end + +@testset "permode" begin + p = 5u"m^3*s^2/H/kg^4" + @test latexify(p) == LaTeXString( + raw"$5\;\mathrm{m}^{3}\,\mathrm{s}^{2}\,\mathrm{kg}^{-4}\,\mathrm{H}^{-1}$" + ) + @test latexify(p; permode=:power) == LaTeXString( + raw"$5\;\mathrm{m}^{3}\,\mathrm{s}^{2}\,\mathrm{kg}^{-4}\,\mathrm{H}^{-1}$" + ) + @test latexify(p; permode=:slash) == LaTeXString( + raw"$5\;\mathrm{m}^{3}\,\mathrm{s}^{2}\,/\,\mathrm{kg}^{4}\,\mathrm{H}$" + ) + @test latexify(p; permode=:frac) == LaTeXString( + raw"$5\;\frac{\mathrm{m}^{3}\,\mathrm{s}^{2}}{\mathrm{kg}^{4}\,\mathrm{H}}$" + ) + @test latexify(p; permode=:frac, fmt=SiunitxNumberFormatter()) == + latexify(p; fmt=SiunitxNumberFormatter()) + @test latexify(m; permode=:frac) == latexify(m) + @test latexify(u"m^-1"; permode=:frac) == LaTeXString(raw"$\frac{1}{\mathrm{m}}$") + @test_throws ErrorException latexify(p; permode=:wrong) +end + +@testset "Labels" begin + @test latexify("x", m) == raw"$x\;\left/\;\mathrm{m}\right.$" + @test latexify("x", m; labelformat=:slash) == raw"$x\;\left/\;\mathrm{m}\right.$" + @test latexify("x", m; labelformat=:square) == raw"$x\;\left[\mathrm{m}\right]$" + @test latexify("x", m; labelformat=:round) == raw"$x\;\left(\mathrm{m}\right)$" + @test latexify("x", m; labelformat=:frac) == raw"$\frac{x}{\mathrm{m}}$" + Latexify.set_default(labelformat=:square) + @test latexify("x", m) == raw"$x\;\left[\mathrm{m}\right]$" + @test_throws "Unknown labelformat" latexify("x", m; labelformat=:wrong) +end + +@testset "Parentheses" begin + @test @latexify($(3u"mm")^2 - 4 * $(2u"mm^2")) == + raw"$\left( 3\;\mathrm{mm} \right)^{2} - 4 \cdot 2\;\mathrm{mm}^{2}$" +end +end + + @testset "DimensionError message" begin function errorstr(e) b = IOBuffer()