25 Commits
Author SHA1 Message Date
adrien c2675f5117 Added imperial to README 2026-04-23 00:28:49 +02:00
adrien f26f6086ca Added Imperial unit scales 2026-04-23 00:14:57 +02:00
adrien a91a509368 Added GNU License 2026-04-23 00:00:32 +02:00
adrien 5e7b7c5302 Added some constants 2026-04-22 23:53:43 +02:00
adrien 719679aabc Updated README + Renamed mulBy and divBy to mul div 2026-04-22 23:33:58 +02:00
adrien 2c94df7f4a Added overflaw security + made benchmark work (set to max) 2026-04-22 23:11:42 +02:00
adrien 5efa42c2e1 Made possible to use comptime float, int and T for Scalar operation of Vector 2026-04-22 22:34:46 +02:00
adrien 1129acc542 Same for Scalar 2026-04-22 22:10:57 +02:00
adrien 9544bb584a Removed s and d in Vector (duplicate of dims and scales) 2026-04-22 22:10:04 +02:00
adrien 6c50a01d6e Removed ugly Scalar_ 2026-04-22 22:08:05 +02:00
adrien e4d55e36ab Added the posibility to use comptime_float, int and T for Scalar operation 2026-04-22 16:45:02 +02:00
adrien 82bdb96746 Changed how Scalar is generated to use Dimensions.ArgOpts and Scales.ArgOpts 2026-04-22 16:31:07 +02:00
adrien a2d46e3f55 Added sqrt to Scalar and Vector 2026-04-22 15:43:17 +02:00
adrien 0dd9e02f59 Added abs, pow and product operations 2026-04-22 14:50:21 +02:00
adrien c4e462add3 Added dot and cross function for Vector 2026-04-22 12:32:10 +02:00
adrien 4c910319d5 Added comparison operation to Scalar and Vector 2026-04-22 12:26:17 +02:00
adrien 86841318f2 Added some doc comments 2026-04-22 10:58:14 +02:00
adrien ec05b60fc3 Replaced Scales.min to helper.finerScales 2026-04-22 10:31:04 +02:00
adrien 49b56bda91 Added int nanometer example in README 2026-04-22 01:20:09 +02:00
adrien 76c69448de Fixed benchmark with removed scale 2026-04-22 01:14:38 +02:00
adrien cb3c41cf4c Better README 2026-04-22 01:07:06 +02:00
adrien 5f42a8c107 Added Dimless to Base 2026-04-22 01:06:52 +02:00
adrien 1719b0de8b Removed scale (just use mulBy + dimless) + added inline to vector fn 2026-04-22 01:05:33 +02:00
adrien 84d0698878 Removed ztracy dep 2026-04-22 00:31:37 +02:00
adrien 11a741f72c Removed useless code 2026-04-22 00:19:07 +02:00
11 changed files with 2444 additions and 544 deletions
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+293 -215
View File
@@ -1,268 +1,346 @@
# zig_units
# dimal — Dimensional Analysis for Zig
`zig_units` lets you attach physical units to numeric values so that dimension mismatches (like adding distance to time) become **compile errors** rather than silent bugs.
A comptime-first dimensional analysis module for Zig. If you try to add meters to seconds, **it won't compile**. That's the point.
At runtime, a `Quantity` is just its underlying numeric value — **zero memory overhead.**
Started by a space simulation where `i128` positions were needed to avoid float imprecision far from the origin, this module grew into a full physical-unit type system with zero runtime overhead.
```zig
const velocity = distance.divBy(time); // Result type: L¹T⁻¹ ✓
const error = mass.add(velocity); // COMPILE ERROR: M¹ != L¹T⁻¹
```
> **Source:** [git.bouvais.lu/adrien/zig-dimal](https://git.bouvais.lu/adrien/zig-dimal)
> **Minimum Zig version:** `0.16.0`
**Requirements:** Zig `0.16.0`
---
## Features
- **100% comptime** — all dimension and unit tracking happens at compile time. No added memory, *almost* native performance.
- **Compile-time dimension errors** — adding `Meter` to `Second` is a compile error, not a runtime panic.
- **Automatic unit conversion** — use `.to()` to convert between compatible units (e.g. `km/h` → `m/s`). Scale factors are resolved at comptime.
* **Full SI prefix & Imperial support** — `pico` through `peta`, plus common Imperial units like `inch`, `ft`, `mi`, `lb`, and `oz`.
- **Time scale support** — `min`, `hour`, `year` built in.
- **Scalar and Vector types** — operate on individual values or fixed-size arrays with the same dimensional safety.
- **Built-in physical quantities** — `dma.Base` provides ready-made types for `Velocity`, `Acceleration`, `Force`, `Energy`, `Pressure`, `ElectricCharge`, `ThermalConductivity`, and many more.
- **Comparison operations** — `eq`, `ne`, `gt`, `gte`, `lt`, `lte` on both `Scalar` and `Vector`, with automatic scale resolution.
- **Arithmetic with bare numbers** — multiply or divide a dimensioned value by a `comptime_int`, `comptime_float`, or plain `T` directly. The value is treated as dimensionless; dimensions pass through unchanged.
- **`abs`, `pow`, `sqrt`** — unary operations with correct dimension tracking (`pow(2)` on `L¹` → `L²`, etc.).
- **Vector geometry** — `dot` product (returns a `Scalar`), `cross` product (Vec3 only), element-wise `product` (all components multiplied).
- **Rich formatting** — values print with their unit automatically: `9.81m.s⁻²`, `42m.kg.s⁻¹`, `0.172km`.
- **`i128` support** — the whole reason this exists. Use large integers for high-precision fixed-point positions without manual conversion.
- **Tests and benchmarks included** — run them and see how it performs on your machine (results welcome!).
---
## The 7 SI Base Dimensions
| Symbol | Dimension | SI Unit |
|--------|----------------------|----------|
| `L` | Length | `m` |
| `M` | Mass | `g` |
| `T` | Time | `s` |
| `I` | Electric Current | `A` |
| `Tp` | Temperature | `K` |
| `N` | Amount of Substance | `mol` |
| `J` | Luminous Intensity | `cd` |
---
## Installation
### 1. Add as a Zig dependency
```bash
zig fetch --save https://github.com/YOUR_USERNAME/zig_units/archive/refs/heads/main.tar.gz
### 1. Fetch the dependency
```sh
zig fetch --save git+https://git.bouvais.lu/adrien/zig-dimal#0.1.1
```
### 2. Configure `build.zig`
```zig
const zig_units = b.dependency("zig_units", .{
.target = target,
.optimize = optimize,
});
// Add to your module or executable
exe.root_module.addImport("units", zig_units.module("zig_units"));
```
---
## Quick Start: Using Predefined Quantities
`units.Base` provides a clean way to instantiate common physical types without manually defining dimensions.
### 2. Wire it up in `build.zig`
```zig
const std = @import("std");
const units = @import("units");
pub fn main() !void {
// Instantiate types for f32 backing
const Meter = units.Base.Meter.Of(f32);
const Second = units.Base.Second.Of(f32);
const dist = Meter{ .value = 10.0 };
const time = Second{ .value = 2.0 };
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const dimal = b.dependency("dimal", .{}).module("dimal");
// Arithmetic is type-safe and creates the correct resulting dimension
const vel = dist.divBy(time); // Type is Velocity (L/T)
std.debug.print("Speed: {f}\n", .{vel}); // Output: 5m.s⁻¹
const exe = b.addExecutable(.{
.name = "my_project",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.imports = &.{.{
.name = "dimal",
.module = dimal,
}},
}),
});
b.installArtifact(exe);
}
```
---
### Core Arithmetic Operations
Dimensional analysis is handled entirely at compile-time. If the math doesn't make physical sense, it won't compile.
### 3. Import in your code
```zig
const M = units.Base.Meter.Of(f32);
const S = units.Base.Second.Of(f32);
const dist = M{ .value = 100.0 };
const time = S{ .value = 5.0 };
// 1. Addition & Subtraction (Must have same dimensions)
const two_dist = dist.add(dist); // 200.0m
const zero = dist.sub(dist); // 0.0m
// 2. Division (Subtracts dimension exponents)
// Result: Velocity (L¹ T⁻¹)
const vel = dist.divBy(time); // 20.0 m.s⁻¹
// 3. Multiplication (Adds dimension exponents)
// Result: Area (L²)
const area = dist.mulBy(dist); // 100.0 m²
// 4. Chained Operations
// Result: Acceleration (L¹ T⁻²)
const accel = dist.divBy(time).divBy(time); // 4.0 m.s⁻²
const dma = @import("dimal");
const Scalar = dma.Scalar;
const Dimensions = dma.Dimensions;
const Scales = dma.Scales;
```
---
## Defining Custom Quantities
## Quick Start
You aren't limited to the built-in library. You can define any physical quantity by specifying its **Dimensions**
(powers of base units) and its **Scale** (SI prefixes).
### Defining unit types
### 1. Create a custom dimension
Dimensions are defined by 7 base SI units: `L` (Length), `M` (Mass), `T` (Time), `I` (Current), `Tp` (Temp), `N` (Substance), `J` (Intensity).
A `Scalar` type is parameterized by three things: the numeric type (`f64`, `i128`, …), the dimensions (which physical quantities and their exponents), and the scales (SI prefixes or custom time units). Both the dimension and scale arguments are plain struct literals — no wrapper call needed.
```zig
const Dims = units.Dimensions;
const Scales = units.Scales;
// Frequency is T⁻¹
const FreqDims = Dims.init(.{ .T = -1 });
// Force is M¹ L¹ T⁻²
const ForceDims = Dims.init(.{ .M = 1, .L = 1, .T = -2 });
const Meter = Scalar(f64, .{ .L = 1 }, .{});
const NanoMeter = Scalar(i64, .{ .L = 1 }, .{ .L = .n });
const KiloMeter = Scalar(f64, .{ .L = 1 }, .{ .L = .k });
const Second = Scalar(f64, .{ .T = 1 }, .{});
const Velocity = Scalar(f64, .{ .L = 1, .T = -1 }, .{});
const Kmh = Scalar(f64, .{ .L = 1, .T = -1 }, .{ .L = .k, .T = .hour });
```
### 2. Create a custom Type
Combine a numeric type, the dimensions, and a scale.
Or use the pre-built helpers from `dma.Base`:
```zig
const Hertz = units.Quantity(f32, FreqDims, Scales.init(.{}));
const Acceleration = dma.Base.Acceleration.Of(f64);
const KmhSpeed = dma.Base.Speed.Scaled(f64, .{ .L = .k, .T = .hour });
```
// A specialized scale: Millimeters per Second Squared
const MmPerSecSq = units.Quantity(f32,
Dims.init(.{ .L = 1, .T = -2 }),
Scales.init(.{ .L = .m }) // .m = milli
);
### Kinematics example
```zig
const v0 = Velocity{ .value = 10.0 }; // 10 m/s
const accel = Acceleration{ .value = 9.81 }; // 9.81 m/s²
const time = Second{ .value = 5.0 }; // 5 s
// d = v₀t + ½at²
const d1 = v0.mul(time); // → Meter
const d2 = accel.mul(time).mul(time).mul(0.5); // → Meter (bare 0.5 is dimensionless)
const dist = d1.add(d2);
const v_final = v0.add(accel.mul(time));
std.debug.print("Distance: {d} | {d}\n", .{ dist, dist.to(KiloMeter) });
// Distance: 172.625m | 0.172625km
std.debug.print("Final speed: {d:.2}\n", .{v_final});
// Final speed: 59.05m.s⁻¹
```
### Unit conversion
`.to()` converts between compatible units at comptime. Mixing incompatible dimensions is a **compile error**.
```zig
const speed_kmh = Kmh{ .value = 120.0 };
const speed_ms = speed_kmh.to(Velocity); // 33.333... m/s — comptime ratio
// This would NOT compile:
// const bad = speed_kmh.to(Second); // "Dimension mismatch in to: L1T-1 vs T1"
```
#### Imperial
```zig
const Inch = Scalar(f64, .{ .L = 1 }, .{ .L = .inch });
const Mile = Scalar(f64, .{ .L = 1 }, .{ .L = .mi });
const Pound = Scalar(f64, .{ .M = 1 }, .{ .M = .lb });
// Conversion example
const dist_m = Meter{ .value = 1609.344 };
const dist_mi = dist_m.to(Mile); // Result: 1.0
```
### Arithmetic with bare numbers
Passing a `comptime_int`, `comptime_float`, or plain `T` to `mul` / `div` treats it as a dimensionless value. Dimensions pass through unchanged.
```zig
const Meter = Scalar(f64, .{ .L = 1 }, .{});
const d = Meter{ .value = 6.0 };
const half = d.mul(0.5); // comptime_float → still Meter
const doubled = d.mul(2); // comptime_int → still Meter
const factor: f64 = 3.0;
const tripled = d.mul(factor); // runtime f64 → still Meter
```
### Comparisons
`eq`, `ne`, `gt`, `gte`, `lt`, `lte` work on any two `Scalar` values of the **same dimension**. Scales are resolved automatically before comparing.
```zig
const Meter = Scalar(i64, .{ .L = 1 }, .{});
const KiloMeter = Scalar(i64, .{ .L = 1 }, .{ .L = .k });
const m1000 = Meter{ .value = 1000 };
const km1 = KiloMeter{ .value = 1 };
const km2 = KiloMeter{ .value = 2 };
_ = m1000.eq(km1); // true — same magnitude
_ = km2.gt(m1000); // true — 2 km > 1000 m
_ = m1000.lte(km2); // true
// Comparing with a bare number works when the scalar is dimensionless.
// Comparing incompatible dimensions is a compile error.
```
### Unary operations: `abs`, `pow`, `sqrt`
```zig
const Meter = Scalar(f64, .{ .L = 1 }, .{});
const d = Meter{ .value = -4.0 };
const magnitude = d.abs(); // 4.0 m — dimension unchanged
const area = d.pow(2); // 16.0 m² — dims scaled by exponent
const side = area.sqrt(); // 4.0 m — dims halved (requires even exponents)
```
`pow` accepts any `comptime_int` exponent and adjusts the dimension exponents accordingly. `sqrt` is a compile error unless all dimension exponents are even.
### Working with Vectors
Every `Scalar` type exposes a `.Vec3` alias and a generic `.Vec(n)` type accessor:
```zig
const Vec3Meter = Meter.Vec3; // equivalent to Vector(3, Meter)
const pos = Vec3Meter{ .data = .{ 100, 200, 300 } };
const t = Second{ .value = 10 };
const vel = pos.divScalar(t); // → Vec3 of Velocity (m/s)
std.debug.print("{d}\n", .{vel}); // (10, 20, 30)m.s⁻¹
```
#### Dot and cross products
```zig
const Newton = Scalar(f32, .{ .M = 1, .L = 1, .T = -2 }, .{});
const r = Meter.Vec3{ .data = .{ 10.0, 0.0, 0.0 } };
const force = Newton.Vec3{ .data = .{ 5.0, 5.0, 0.0 } };
// Dot product — returns a Scalar (dimensions summed)
const work = force.dot(r); // 50.0 J (M¹L²T⁻²)
// Cross product — returns a Vec3 (dimensions summed, Vec3 only)
const torque = r.cross(force); // (0, 0, 50) N·m
```
#### Vector comparisons
Element-wise comparisons return `[len]bool`. Whole-vector equality uses `eqAll` / `neAll`. A single scalar can be broadcast with the `*Scalar` variants.
```zig
const positions = Meter.Vec3{ .data = .{ 500.0, 1200.0, 3000.0 } };
const threshold = KiloMeter{ .value = 1.0 }; // 1 km
const exceeded = positions.gtScalar(threshold); // [false, true, true]
const eq_each = positions.eq(positions); // [true, true, true] (element-wise)
const all_same = positions.eqAll(positions); // true (whole-vector)
```
#### Other Vector operations
```zig
const v = Meter.Vec3{ .data = .{ -2.0, 3.0, -4.0 } };
const v_abs = v.abs(); // { 2, 3, 4 } m
const vol = v_abs.product(); // 24 m³ (dims × len)
const area = v_abs.pow(2); // { 4, 9, 16 } m²
const sides = area.sqrt(); // { 2, 3, 4 } m (element-wise sqrt)
```
---
## Unit Conversions
## API Reference
The library handles SI prefixes (`k`, `m`, `u`, `n`, etc.) and time aliases (`.min`, `.hour`) automatically.
When performing arithmetic between different scales, the **finer (smaller) scale wins** to preserve precision.
### `Scalar(T, dims, scales)`
```zig
const KM = units.Base.Meter.Scaled(f32, Scales.init(.{ .L = .k })); // Kilometers
const M = units.Base.Meter.Of(f32); // Meters
| Method | Description |
|---|---|
| `.add(rhs)` | Add two quantities of the same dimension. Auto-converts scales. |
| `.sub(rhs)` | Subtract. Auto-converts scales. |
| `.mul(rhs)` | Multiply — dimensions are **summed**. `rhs` may be a `Scalar`, `T`, `comptime_int`, or `comptime_float` (bare numbers are dimensionless). |
| `.div(rhs)` | Divide — dimensions are **subtracted**. Same `rhs` flexibility as `mul`. |
| `.abs()` | Absolute value. Dimensions and scales unchanged. |
| `.pow(exp)` | Raise to a `comptime_int` exponent. Dimension exponents are multiplied by `exp`. |
| `.sqrt()` | Square root. Compile error unless all dimension exponents are even. |
| `.eq(rhs)` / `.ne(rhs)` | Equality / inequality comparison. Scales auto-resolved. |
| `.gt(rhs)` / `.gte(rhs)` | Greater-than / greater-than-or-equal. |
| `.lt(rhs)` / `.lte(rhs)` | Less-than / less-than-or-equal. |
| `.to(DestType)` | Convert to another unit of the same dimension. Compile error on mismatch. |
| `.vec(len)` | Return a `Vector(len, Self)` with all components set to this value. |
| `.vec3()` | Shorthand for `.vec(3)`. |
| `.Vec3` | Type alias for `Vector(3, Self)`. |
const d1 = KM{ .value = 1.2 }; // 1.2 km
const d2 = M{ .value = 300.0 }; // 300 m
### `Vector(len, Q)`
const total = d1.add(d2); // Result is 1500.0 (Meters)
const final = total.to(KM); // Explicitly convert back to KM -> 1.5
```
| Method | Description |
|---|---|
| `.add(rhs)` / `.sub(rhs)` | Element-wise add / subtract. |
| `.mul(rhs)` / `.div(rhs)` | Element-wise multiply / divide (both operands are Vectors). |
| `.mulScalar(s)` / `.divScalar(s)` | Scale every component by a single `Scalar`, `T`, `comptime_int`, or `comptime_float`. |
| `.dot(rhs)` | Dot product → `Scalar` with combined dimensions. |
| `.cross(rhs)` | Cross product → `Vector(3, …)`. Vec3 only. |
| `.abs()` | Element-wise absolute value. |
| `.pow(exp)` | Element-wise `comptime_int` power. Dimension exponents scaled. |
| `.sqrt()` | Element-wise square root. |
| `.product()` | Multiply all components → `Scalar` with dimensions × `len`. |
| `.negate()` | Negate all components. |
| `.length()` | Euclidean length (returns `T`). |
| `.lengthSqr()` | Sum of squared components (returns `T`). Cheaper than `length`. |
| `.eq(rhs)` / `.ne(rhs)` | Element-wise comparison → `[len]bool`. |
| `.gt(rhs)` / `.gte(rhs)` / `.lt(rhs)` / `.lte(rhs)` | Element-wise ordered comparisons → `[len]bool`. |
| `.eqAll(rhs)` / `.neAll(rhs)` | Whole-vector equality / inequality → `bool`. |
| `.eqScalar(s)` / `.neScalar(s)` | Broadcast scalar comparison → `[len]bool`. |
| `.gtScalar(s)` / `.gteScalar(s)` / `.ltScalar(s)` / `.lteScalar(s)` | Broadcast ordered scalar comparisons → `[len]bool`. |
| `.to(DestQ)` | Convert all components to a compatible scalar type. |
### `dma.Base` — Pre-built quantities
Call `.Of(T)` for base-unit scalars, `.Scaled(T, scales)` for custom scales:
`Meter`, `Second`, `Gramm`, `Kelvin`, `ElectricCurrent`, `Speed`, `Acceleration`, `Inertia`, `Force`, `Pressure`, `Energy`, `Power`, `Area`, `Volume`, `Density`, `Frequency`, `Viscosity`, `ElectricCharge`, `ElectricPotential`, `ElectricResistance`, `MagneticFlux`, `ThermalCapacity`, `ThermalConductivity`, and more.
### `Scales` — SI and Imperial Units
| Tag | Factor (Relative to Base) | Type |
|---|---|---|
| `.P` ... `.f` | $10^{15}$ ... $10^{-15}$ | SI Prefixes |
| `.min`, `.hour`, `.year` | 60, 3600, 31,536,000 | Time |
| **`.inch`** | **0.0254** | Imperial Length (m) |
| **`.ft`** | **0.3048** | Imperial Length (m) |
| **`.yd`** | **0.9144** | Imperial Length (m) |
| **`.mi`** | **1609.344** | Imperial Length (m) |
| **`.oz`** | **28.3495231** | Imperial Mass (g) |
| **`.lb`** | **453.59237** | Imperial Mass (g) |
| **`.st`** | **6350.29318** | Imperial Mass (g) |
Scale entries for dimensions with exponent `0` are ignored — multiplying a dimensionless value by a kilometre-scale value no longer accidentally inherits the `k` prefix.
---
## Physical Vectors (Vec3)
## Running Tests and Benchmarks
Physical quantities often come in 3D vectors (Position, Velocity, Force). Every `Quantity` type has a `.Vec3` alias built-in.
```zig
const Vec3M = units.Base.Meter.Of(f32).Vec3;
const gravity = Vec3M{ .data = .{ 0, -9.81, 0 } };
const pos = Vec3M.initDefault(0); // [0, 0, 0]
// Vectors support standard operations
const length = gravity.length(); // Returns f32: 9.81
const double = gravity.scale(2.0);
```
You can also create a Vector of any length.
Vec3 found in a Quantity is just a convenience.
```zig
const M = units.Base.Meter.Of(f32);
const Vec10M = units.QuantityVec(10, Meter);
const gravity = Vec10M.initDefault(1);
const length = gravity.length(); // Returns f32: 1.0
```
---
## High Precision & Integer Backing
While most libraries default to `f32` or `f64`, `zig_units` is mainly designed to support **large-bit integers (`i128`, `i256`)**.
This is critical for applications like **space simulations**, where floating-point numbers suffer from "jitter" or "flickering"
once you travel far from the origin. By using an `i128` with a millimeter scale, you can represent the diameter
of the observable universe with millimeter precision—something impossible with `f64`.
### Avoiding Floating-Point Jitter
```zig
// Millimeter precision using 128-bit integers
const MM = units.Base.Meter.Scaled(i128, units.Scales.init(.{ .L = .m }));
const KM = units.Base.Meter.Scaled(i128, units.Scales.init(.{ .L = .k }));
const solar_system_dist = KM{ .value = 150_000_000 }; // 150 million km
const ship_nudge = MM{ .value = 5 }; // 5 mm
// The library performs exact integer math for conversions.
// Resulting type is MM (the finer scale), maintaining perfect precision.
const new_pos = solar_system_dist.add(ship_nudge);
```
### Integer-Specific Features
* **Exact Conversions:** When converting between integer scales (e.g., `km` to `m`), the library uses fast-path native multiplication.
* **Safe Vector Lengths:** `QuantityVec.length()` includes a custom integer square root implementation, allowing you to calculate distances between coordinates without ever casting to a float.
* **Zero Drift:** Unlike floats, repeated additions and subtractions of integers never accumulate "epsilon" drift, ensuring your simulation remains deterministic.
* **Precision-First Scaling:** When operating on two different scales (e.g., adding `km` and `mm`), the result automatically adopts the finer scale (`mm`). This ensures **zero implicit data loss** during calculation. You only lose precision if you *explicitly* choose to convert back to a coarser scale using `.to()`.
---
## SI Scales Reference
| Prefix | Enum | Factor |
| :--- | :--- | :--- |
| **Kilo** | `.k` | 10³ |
| **Mega** | `.M` | 10⁶ |
| **Giga** | `.G` | 10⁹ |
| **Milli** | `.m` | 10⁻³ |
| **Micro** | `.u` | 10⁻⁶ |
| **Minute**| `.min` | 60 |
| **Hour** | `.hour`| 3,600 |
---
## API Summary
### `Quantity(T, dims, scales)`
- `.add(rhs)` / `.sub(rhs)`: Automatic scaling, requires same dimensions.
- `.mulBy(rhs)` / `.divBy(rhs)`: Composes dimensions (e.g., $L \times L = L^2$).
- `.scale(scalar)`: Multiply by a raw number (preserves dimensions).
- `.to(OtherType)`: Safely convert between scales of the same dimension.
- `.vec3()`: Create a 3D vector from a scalar.
### `Dimensions`
- `L`: Length (m)
- `M`: Mass (g)
- `T`: Time (s)
- `I`: Current (A)
- `Tp`: Temperature (K)
- `N`: Amount (mol)
- `J`: Intensity (cd)
---
## Testing & Benchmarks
`zig_units` comes with a comprehensive test suite that verifies dimensional correctness, SI prefix scaling,
and vector math accuracy across all numeric types.
### Running Tests
To run the full suite of unit tests and performance benchmarks:
```bash
```sh
zig build test
zig build benchmark
```
### Benchmarks
When you run the tests, the library also executes a performance benchmark.
This measures the cost of operations (in nanoseconds per operation) across different backing types (`i32` to `f128`) and vector lengths.
Benchmark results are very welcome — feel free to share yours!
Because all dimensional logic is resolved at **compile-time**, you will see that `Quantity` operations perform at the same speed as raw primitive math.
---
**Example Benchmark Output:**
```text
Quantity<T> benchmark — 100,000 iterations, 10 samples/cell
## Roadmap / Known Limitations
┌───────────────────┬──────┬─────────────────────┬─────────────────────┐
│ Operation │ Type │ ns / op (± delta) │ Throughput (ops/s) │
├───────────────────┼──────┼─────────────────────┼─────────────────────┤
│ add │ i32 │ 0.18 ns ±0.02 │ 5555555556 │
│ mulBy │ f64 │ 0.22 ns ±0.01 │ 4545454545 │
└───────────────────┴──────┴─────────────────────┴─────────────────────┘
```
- SIMD acceleration for `Vector` operations.
- Some paths may still fall back to runtime computation — optimization ongoing.
- More test coverage.
### Verification Examples
The test suite ensures that:
- **Dimension Safety:** Chained operations like `dist.divBy(time).divBy(time)` correctly result in an Acceleration type.
- **Scale Accuracy:** Adding `1km + 1mm` results exactly in `1000001mm` without truncation.
- **Formatting:** Quantities print correctly with Unicode superscripts (e.g., `9.81m.s⁻²`).
- **Vector Math:** Euclidean lengths for both floats and integers are verified against known constants.
---
## License
See the repository for license details.
+3 -26
View File
@@ -7,7 +7,8 @@ pub fn build(b: *std.Build) void {
// 1. Define the module so other projects can import it
_ = b.addModule("dimal", .{
.root_source_file = b.path("src/main.zig"),
}); // 2. Keep the test setup
});
const exe_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
@@ -21,24 +22,6 @@ pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_exe_tests.step);
const options = .{
.enable_ztracy = b.option(
bool,
"enable_ztracy",
"Enable Tracy profile markers",
) orelse false,
.enable_fibers = b.option(
bool,
"enable_fibers",
"Enable Tracy fiber support",
) orelse false,
.on_demand = b.option(
bool,
"on_demand",
"Build tracy with TRACY_ON_DEMAND",
) orelse false,
};
const bench_exe = b.addExecutable(.{
.name = "benchmark",
.root_module = b.createModule(.{
@@ -48,13 +31,7 @@ pub fn build(b: *std.Build) void {
}),
});
const ztracy = b.dependency("ztracy", .{
.enable_ztracy = options.enable_ztracy,
.enable_fibers = options.enable_fibers,
.on_demand = options.on_demand,
});
bench_exe.root_module.addImport("ztracy", ztracy.module("root"));
bench_exe.root_module.linkLibrary(ztracy.artifact("tracy"));
b.installArtifact(bench_exe);
const run_bench = b.addRunArtifact(bench_exe);
const bench_step = b.step("benchmark", "Run the benchmark suite");
+1 -6
View File
@@ -3,12 +3,7 @@
.version = "0.1.0",
.fingerprint = 0x9453b1ff1e52d858,
.minimum_zig_version = "0.16.0",
.dependencies = .{
.ztracy = .{
.url = "git+https://github.com/zig-gamedev/ztracy#d95d2a193b9f97944a17026c9a42b5d0f9a88c94",
.hash = "ztracy-0.14.0-dev-zHJSqzUHGQAmhJybhlwtl1QKevUBw4M5YKZqPfWx2y99",
},
},
.dependencies = .{},
.paths = .{
"build.zig",
"build.zig.zon",
+114 -16
View File
@@ -5,29 +5,90 @@ const Dimensions = @import("Dimensions.zig");
const Scales = @import("Scales.zig");
const Scalar = @import("Scalar.zig").Scalar;
/// Helper function to create a clean namespace for each physical dimension.
/// It exposes the raw dimensions, and easy type-creators for Base or Scaled variants.
pub fn BaseScalar(comptime d: anytype) type {
fn PhysicalConstant(comptime d: Dimensions.ArgOpts, comptime val: f64, comptime s: Scales.ArgOpts) type {
return struct {
pub const dims = Dimensions.init(d);
const dims = Dimensions.init(d);
const scales = Scales.init(s);
/// Instantiates the constant into a specific numeric type.
pub fn Of(comptime T: type) Scalar(T, d, s) {
return .{ .value = @as(T, @floatCast(val)) };
}
};
}
fn BaseScalar(comptime d: Dimensions.ArgOpts) type {
return struct {
const dims = Dimensions.init(d);
/// Creates a Scalar of this dimension using default scales.
/// Example: const V = Quantities.Velocity.Base(f32);
pub fn Of(comptime T: type) type {
return Scalar(T, dims, Scales.init(.{}));
return Scalar(T, d, .{});
}
/// Creates a Scalar of this dimension using custom scales.
/// Example: const Kmh = Quantities.Velocity.Scaled(f32, Scales.init(.{ .L = .k, .T = .hour }));
pub fn Scaled(comptime T: type, comptime s: Scales) type {
return Scalar(T, dims, s);
pub fn Scaled(comptime T: type, comptime s: Scales.ArgOpts) type {
return Scalar(T, d, s);
}
};
}
// ==========================================
// Physical Constants
// ==========================================
pub const Constants = struct {
/// Speed of light in vacuum (c) [m/s]
pub const SpeedOfLight = PhysicalConstant(.{ .L = 1, .T = -1 }, 299792458.0, .{});
/// Planck constant (h) [J⋅s = kg⋅m²⋅s⁻¹]
pub const Planck = PhysicalConstant(.{ .M = 1, .L = 2, .T = -1 }, 6.62607015e-34, .{ .M = .k });
/// Reduced Planck constant (ℏ) [J⋅s]
pub const ReducedPlanck = PhysicalConstant(.{ .M = 1, .L = 2, .T = -1 }, 1.054571817e-34, .{ .M = .k });
/// Boltzmann constant (k_B) [J⋅K⁻¹ = kg⋅m²⋅s⁻²⋅K⁻¹]
pub const Boltzmann = PhysicalConstant(.{ .M = 1, .L = 2, .T = -2, .Tp = -1 }, 1.380649e-23, .{ .M = .k });
/// Newtonian constant of gravitation (G) [m³⋅kg⁻¹⋅s⁻²]
pub const Gravitational = PhysicalConstant(.{ .M = -1, .L = 3, .T = -2 }, 6.67430e-11, .{ .M = .k });
/// Stefan–Boltzmann constant (σ) [W⋅m⁻²⋅K⁻⁴ = kg⋅s⁻³⋅K⁻⁴]
pub const StefanBoltzmann = PhysicalConstant(.{ .M = 1, .T = -3, .Tp = -4 }, 5.670374419e-8, .{ .M = .k });
/// Elementary charge (e) [C = A⋅s]
pub const ElementaryCharge = PhysicalConstant(.{ .T = 1, .I = 1 }, 1.602176634e-19, .{});
/// Vacuum magnetic permeability (μ_0) [N⋅A⁻² = kg⋅m⋅s⁻²⋅A⁻²]
pub const VacuumPermeability = PhysicalConstant(.{ .M = 1, .L = 1, .T = -2, .I = -2 }, 1.25663706127e-6, .{ .M = .k });
/// Vacuum electric permittivity (ε_0) [F⋅m⁻¹ = A²⋅s⁴⋅kg⁻¹⋅m⁻³]
pub const VacuumPermittivity = PhysicalConstant(.{ .M = -1, .L = -3, .T = 4, .I = 2 }, 8.8541878188e-12, .{ .M = .k });
/// Electron mass (m_e) [kg]
pub const ElectronMass = PhysicalConstant(.{ .M = 1 }, 9.1093837139e-31, .{ .M = .k });
/// Proton mass (m_p) [kg]
pub const ProtonMass = PhysicalConstant(.{ .M = 1 }, 1.67262192595e-27, .{ .M = .k });
/// Neutron mass (m_n) [kg]
pub const NeutronMass = PhysicalConstant(.{ .M = 1 }, 1.67492750056e-27, .{ .M = .k });
/// Fine-structure constant (α) [Dimensionless]
pub const FineStructure = PhysicalConstant(.{}, 0.0072973525643, .{});
/// Avogadro constant (N_A) [mol⁻¹]
/// Note: Assuming mol is currently treated as dimensionless in the base system,
/// otherwise requires adding an `.N` dimension to Dimensions.ArgOpts.
pub const Avogadro = PhysicalConstant(.{}, 6.02214076e23, .{});
};
// ==========================================
// Base Quantities
// ==========================================
pub const Dimless = BaseScalar(.{});
pub const Meter = BaseScalar(.{ .L = 1 });
pub const Second = BaseScalar(.{ .T = 1 });
pub const Gramm = BaseScalar(.{ .M = 1 });
@@ -102,7 +163,7 @@ test "BaseQuantities - Core dimensions instantiation" {
try std.testing.expectEqual(0, M.dims.get(.T));
// Test specific scale variants
const Kmh = Speed.Scaled(f32, Scales.init(.{ .L = .k, .T = .hour }));
const Kmh = Speed.Scaled(f32, .{ .L = .k, .T = .hour });
const speed = Kmh{ .value = 120.0 };
try std.testing.expectEqual(120.0, speed.value);
try std.testing.expectEqual(.k, @TypeOf(speed).scales.get(.L));
@@ -114,31 +175,31 @@ test "BaseQuantities - Kinematics equations" {
const t = Second.Of(f32){ .value = 2.0 };
// Velocity = Distance / Time
const v = d.divBy(t);
const v = d.div(t);
try std.testing.expectEqual(25.0, v.value);
try std.testing.expect(Speed.dims.eql(@TypeOf(v).dims));
// Acceleration = Velocity / Time
const a = v.divBy(t);
const a = v.div(t);
try std.testing.expectEqual(12.5, a.value);
try std.testing.expect(Acceleration.dims.eql(@TypeOf(a).dims));
}
test "BaseQuantities - Dynamics (Force and Work)" {
// 10 kg
const m = Gramm.Scaled(f32, Scales.init(.{ .M = .k })){ .value = 10.0 };
const m = Gramm.Scaled(f32, .{ .M = .k }){ .value = 10.0 };
// 9.8 m/s^2
const a = Acceleration.Of(f32){ .value = 9.8 };
// Force = mass * acceleration
const f = m.mulBy(a);
try std.testing.expectEqual(98000, f.value);
const f = m.mul(a);
try std.testing.expectEqual(98, f.value);
try std.testing.expect(Force.dims.eql(@TypeOf(f).dims));
// Energy (Work) = Force * distance
const distance = Meter.Of(f32){ .value = 5.0 };
const energy = f.mulBy(distance);
try std.testing.expectEqual(490000, energy.value);
const energy = f.mul(distance);
try std.testing.expectEqual(490, energy.value);
try std.testing.expect(Energy.dims.eql(@TypeOf(energy).dims));
}
@@ -147,7 +208,44 @@ test "BaseQuantities - Electric combinations" {
const time = Second.Of(f32){ .value = 3.0 }; // 3 s
// Charge = Current * time
const charge = current.mulBy(time);
const charge = current.mul(time);
try std.testing.expectEqual(6.0, charge.value);
try std.testing.expect(ElectricCharge.dims.eql(@TypeOf(charge).dims));
}
test "Constants - Initialization and dimension checks" {
// Speed of Light
const c = Constants.SpeedOfLight.Of(f64);
try std.testing.expectEqual(299792458.0, c.value);
try std.testing.expectEqual(1, @TypeOf(c).dims.get(.L));
try std.testing.expectEqual(-1, @TypeOf(c).dims.get(.T));
// Electron Mass (verifying scale as well)
const me = Constants.ElectronMass.Of(f64);
try std.testing.expectEqual(9.1093837139e-31, me.value);
try std.testing.expectEqual(1, @TypeOf(me).dims.get(.M));
try std.testing.expectEqual(.k, @TypeOf(me).scales.get(.M)); // Should be scaled to kg
// Boltzmann Constant (Complex derived dimensions)
const kb = Constants.Boltzmann.Of(f64);
try std.testing.expectEqual(1.380649e-23, kb.value);
try std.testing.expectEqual(1, @TypeOf(kb).dims.get(.M));
try std.testing.expectEqual(2, @TypeOf(kb).dims.get(.L));
try std.testing.expectEqual(-2, @TypeOf(kb).dims.get(.T));
try std.testing.expectEqual(-1, @TypeOf(kb).dims.get(.Tp));
try std.testing.expectEqual(.k, @TypeOf(kb).scales.get(.M));
// Vacuum Permittivity
const eps0 = Constants.VacuumPermittivity.Of(f64);
try std.testing.expectEqual(8.8541878188e-12, eps0.value);
try std.testing.expectEqual(-1, @TypeOf(eps0).dims.get(.M));
try std.testing.expectEqual(-3, @TypeOf(eps0).dims.get(.L));
try std.testing.expectEqual(4, @TypeOf(eps0).dims.get(.T));
try std.testing.expectEqual(2, @TypeOf(eps0).dims.get(.I));
// Fine Structure Constant (Dimensionless)
const alpha = Constants.FineStructure.Of(f64);
try std.testing.expectEqual(0.0072973525643, alpha.value);
try std.testing.expectEqual(0, @TypeOf(alpha).dims.get(.M));
try std.testing.expectEqual(0, @TypeOf(alpha).dims.get(.L));
}
+47 -3
View File
@@ -1,5 +1,15 @@
const std = @import("std");
pub const ArgOpts = struct {
L: comptime_int = 0,
M: comptime_int = 0,
T: comptime_int = 0,
I: comptime_int = 0,
Tp: comptime_int = 0,
N: comptime_int = 0,
J: comptime_int = 0,
};
pub const Dimension = enum {
/// Length
L,
@@ -31,11 +41,15 @@ pub const Dimension = enum {
// --------- Dimensions struct ---------
/// Holds the exponent of each SI base dimension for a given quantity (e.g. velocity = L¹T⁻¹).
/// All values are `comptime_int` — no runtime storage.
const Self = @This();
data: std.EnumArray(Dimension, comptime_int),
pub fn init(comptime init_val: anytype) Self {
/// Create a `Dimensions` from a struct literal, e.g. `.{ .L = 1, .T = -1 }`.
/// Unspecified dimensions default to 0.
pub fn init(comptime init_val: ArgOpts) Self {
var s = Self{ .data = std.EnumArray(Dimension, comptime_int).initFill(0) };
inline for (std.meta.fields(@TypeOf(init_val))) |f|
s.data.set(@field(Dimension, f.name), @field(init_val, f.name));
@@ -54,27 +68,57 @@ pub fn set(comptime self: *Self, comptime key: Dimension, comptime val: i8) void
self.data.set(key, val);
}
pub fn argsOpt(self: Self) ArgOpts {
var args: ArgOpts = undefined;
inline for (std.enums.values(Dimension)) |d|
@field(args, @tagName(d)) = self.get(d);
return args;
}
/// Add exponents component-wise. Used internally by `mul`.
pub fn add(comptime a: Self, comptime b: Self) Self {
var result = Self.initFill(0);
for (std.enums.values(Dimension)) |d|
inline for (std.enums.values(Dimension)) |d|
result.set(d, a.get(d) + b.get(d));
return result;
}
/// Subtract exponents component-wise. Used internally by `div`.
pub fn sub(comptime a: Self, comptime b: Self) Self {
@setEvalBranchQuota(10_000);
var result = Self.initFill(0);
inline for (std.enums.values(Dimension)) |d|
result.set(d, a.get(d) - b.get(d));
return result;
}
/// Multiply exponents by a scalar integer. Used internally by `pow` in Scalar.
pub fn scale(comptime a: Self, comptime exp: comptime_int) Self {
var result = Self.initFill(0);
inline for (std.enums.values(Dimension)) |d|
result.set(d, a.get(d) * exp);
return result;
}
pub fn div(comptime a: Self, comptime exp: comptime_int) Self {
var result = Self.initFill(0);
inline for (std.enums.values(Dimension)) |d|
result.set(d, a.get(d) / exp);
return result;
}
/// Returns true if every dimension exponent is equal. Used to enforce type compatibility in `add`, `sub`, `to`.
pub fn eql(comptime a: Self, comptime b: Self) bool {
inline for (std.enums.values(Dimension)) |d|
if (a.get(d) != b.get(d)) return false;
return true;
}
pub fn isSquare(comptime a: Self) bool {
inline for (std.enums.values(Dimension)) |d|
if (a.get(d) % 2 != 0) return false;
return true;
}
pub fn str(comptime a: Self) []const u8 {
var out: []const u8 = "";
const dims = std.enums.values(Dimension);
+526 -113
View File
@@ -7,89 +7,177 @@ const UnitScale = Scales.UnitScale;
const Dimensions = @import("Dimensions.zig");
const Dimension = Dimensions.Dimension;
pub fn Scalar(comptime T: type, comptime d: Dimensions, comptime s: Scales) type {
@setEvalBranchQuota(100_000);
// ---------------------------------------------------------------------------
/// A dimensioned scalar value. `T` is the numeric type, `d` the dimension exponents, `s` the SI scales.
/// All dimension and unit tracking is resolved at comptime — zero runtime overhead.
pub fn Scalar(comptime T: type, comptime d_opt: Dimensions.ArgOpts, comptime s_opt: Scales.ArgOpts) type {
@setEvalBranchQuota(10_000_000);
return struct {
value: T,
const Self = @This();
/// Type of Vector(3, Self)
pub const Vec3: type = Vector(3, Self);
/// Type of underline value, mostly use for Vector
pub const ValueType: type = T;
pub const dims: Dimensions = Dimensions.init(d_opt);
pub const scales = Scales.init(s_opt);
pub const dims: Dimensions = d;
pub const scales = s;
// ---------------------------------------------------------------
// Internal: resolved-rhs shorthands
// ---------------------------------------------------------------
pub inline fn add(self: Self, rhs: anytype) Scalar(
T,
dims,
scales.min(@TypeOf(rhs).scales),
) {
if (comptime !dims.eql(@TypeOf(rhs).dims))
@compileError("Dimension mismatch in add: " ++ dims.str() ++ " vs " ++ @TypeOf(rhs).dims.str());
if (comptime @TypeOf(rhs) == Self)
return .{ .value = self.value + rhs.value };
const TargetType = Scalar(T, dims, scales.min(@TypeOf(rhs).scales));
const lhs_val = if (comptime @TypeOf(self) == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime @TypeOf(rhs) == TargetType) rhs.value else rhs.to(TargetType).value;
return .{ .value = lhs_val + rhs_val };
/// Scalar type that `rhs` normalises to (bare numbers → dimensionless).
inline fn RhsT(comptime Rhs: type) type {
return hlp.rhsScalarType(T, Rhs);
}
pub inline fn sub(self: Self, rhs: anytype) Scalar(
T,
dims,
scales.min(@TypeOf(rhs).scales),
) {
if (comptime !dims.eql(@TypeOf(rhs).dims))
@compileError("Dimension mismatch in sub: " ++ dims.str() ++ " vs " ++ @TypeOf(rhs).dims.str());
if (comptime @TypeOf(rhs) == Self)
return .{ .value = self.value - rhs.value };
const TargetType = Scalar(T, dims, scales.min(@TypeOf(rhs).scales));
const lhs_val = if (comptime @TypeOf(self) == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime @TypeOf(rhs) == TargetType) rhs.value else rhs.to(TargetType).value;
return .{ .value = lhs_val - rhs_val };
/// Normalise `rhs` (bare number or Scalar) into a proper Scalar value.
inline fn rhs(r: anytype) RhsT(@TypeOf(r)) {
return hlp.toRhsScalar(T, r);
}
pub inline fn mulBy(self: Self, rhs: anytype) Scalar(
// ---------------------------------------------------------------
// Arithmetic
// ---------------------------------------------------------------
/// Add two quantities. Dimensions must match — compile error otherwise.
/// Scales are auto-resolved to the finer of the two.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`
/// (bare numbers are treated as dimensionless).
pub inline fn add(self: Self, r: anytype) Scalar(
T,
dims.add(@TypeOf(rhs).dims),
scales.min(@TypeOf(rhs).scales),
dims.argsOpt(),
hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(),
) {
const RhsType = @TypeOf(rhs);
const SelfNorm = Scalar(T, dims, scales.min(RhsType.scales));
const RhsNorm = Scalar(T, RhsType.dims, scales.min(RhsType.scales));
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in add: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime RhsType == Self)
return .{ .value = self.value + rhs_s.value };
const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value;
return .{ .value = if (comptime hlp.isInt(T)) lhs_val +| rhs_val else lhs_val + rhs_val };
}
/// Subtract two quantities. Dimensions must match — compile error otherwise.
/// Scales are auto-resolved to the finer of the two.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn sub(self: Self, r: anytype) Scalar(
T,
dims.argsOpt(),
hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(),
) {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in sub: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime RhsType == Self)
return .{ .value = self.value - rhs_s.value };
const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value;
return .{ .value = if (comptime hlp.isInt(T)) lhs_val -| rhs_val else lhs_val - rhs_val };
}
/// Multiply two quantities. Dimension exponents are summed: `L¹ * T⁻¹ → L¹T⁻¹`.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`
/// (bare numbers are treated as dimensionless — dimensions pass through unchanged).
pub inline fn mul(self: Self, r: anytype) Scalar(
T,
dims.add(RhsT(@TypeOf(r)).dims).argsOpt(),
hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(),
) {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
const SelfNorm = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const RhsNorm = Scalar(T, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
if (comptime Self == SelfNorm and RhsType == RhsNorm)
return .{ .value = self.value * rhs.value };
return .{ .value = self.value * rhs_s.value };
const lhs_val = if (comptime Self == SelfNorm) self.value else self.to(SelfNorm).value;
const rhs_val = if (comptime RhsType == RhsNorm) rhs.value else rhs.to(RhsNorm).value;
return .{ .value = lhs_val * rhs_val };
const rhs_val = if (comptime RhsType == RhsNorm) rhs_s.value else rhs_s.to(RhsNorm).value;
return .{ .value = if (comptime hlp.isInt(T)) lhs_val *| rhs_val else lhs_val * rhs_val };
}
pub inline fn divBy(self: Self, rhs: anytype) Scalar(
/// Divide two quantities. Dimension exponents are subtracted: `L¹ / T¹ → L¹T⁻¹`.
/// Integer types use truncating division.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn div(self: Self, r: anytype) Scalar(
T,
dims.sub(@TypeOf(rhs).dims),
scales.min(@TypeOf(rhs).scales),
dims.sub(RhsT(@TypeOf(r)).dims).argsOpt(),
hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(),
) {
const RhsType = @TypeOf(rhs);
const SelfNorm = Scalar(T, dims, scales.min(RhsType.scales));
const RhsNorm = Scalar(T, RhsType.dims, scales.min(RhsType.scales));
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
const SelfNorm = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const RhsNorm = Scalar(T, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == SelfNorm) self.value else self.to(SelfNorm).value;
const rhs_val = if (comptime RhsType == RhsNorm) rhs.value else rhs.to(RhsNorm).value;
if (comptime @typeInfo(T) == .int) {
const rhs_val = if (comptime RhsType == RhsNorm) rhs_s.value else rhs_s.to(RhsNorm).value;
if (comptime hlp.isInt(T)) {
return .{ .value = @divTrunc(lhs_val, rhs_val) };
} else {
return .{ .value = lhs_val / rhs_val };
}
}
pub inline fn scale(self: Self, sc: T) Self {
return .{ .value = self.value * sc };
// ---------------------------------------------------------------
// Unary
// ---------------------------------------------------------------
/// Returns the absolute value of the quantity.
/// Dimensions and scales remain entirely unchanged.
pub inline fn abs(self: Self) Self {
if (comptime @typeInfo(T) == .int)
return .{ .value = @intCast(@abs(self.value)) }
else
return .{ .value = @abs(self.value) };
}
/// Raises the quantity to a compile-time integer exponent.
/// Dimension exponents are multiplied by the exponent: `(L²)³ → L⁶`.
pub inline fn pow(self: Self, comptime exp: comptime_int) Scalar(
T,
dims.scale(exp).argsOpt(),
scales.argsOpt(),
) {
if (comptime hlp.isInt(T))
return .{ .value = std.math.powi(T, self.value, exp) catch std.math.maxInt(T) }
else
return .{ .value = std.math.pow(T, self.value, @as(T, @floatFromInt(exp))) };
}
pub inline fn sqrt(self: Self) Scalar(
T,
dims.div(2).argsOpt(),
scales.argsOpt(),
) {
if (comptime !dims.isSquare()) // Check if all exponents are divisible by 2
@compileError("Cannot take sqrt of " ++ dims.str() ++ ": exponents must be even.");
if (self.value < 0) return .{ .value = 0 };
if (comptime hlp.isInt(T)) {
const UnsignedT = @Int(.unsigned, @typeInfo(T).int.bits);
const u_len_sq = @as(UnsignedT, @intCast(self.value));
return .{ .value = @as(T, @intCast(std.math.sqrt(u_len_sq))) };
} else {
return .{ .value = @sqrt(self.value) };
}
}
// ---------------------------------------------------------------
// Conversion
// ---------------------------------------------------------------
/// Convert to a compatible unit type. The scale ratio is computed at comptime.
/// Compile error if dimensions don't match.
pub inline fn to(self: Self, comptime Dest: type) Dest {
if (comptime !dims.eql(Dest.dims))
@compileError("Dimension mismatch in to: " ++ dims.str() ++ " vs " ++ Dest.dims.str());
@@ -105,11 +193,10 @@ pub fn Scalar(comptime T: type, comptime d: Dimensions, comptime s: Scales) type
const mult: DestT = comptime @intFromFloat(ratio);
return .{ .value = @as(DestT, @intCast(self.value)) * mult };
} else if (comptime ratio < 1.0 and @round(1.0 / ratio) == 1.0 / ratio) {
const div: DestT = comptime @intFromFloat(1.0 / ratio);
const d: DestT = comptime @intFromFloat(1.0 / ratio);
const val = @as(DestT, @intCast(self.value));
const half = comptime div / 2;
// Native round-to-nearest
const rounded = if (val >= 0) @divTrunc(val + half, div) else @divTrunc(val - half, div);
const half = comptime d / 2;
const rounded = if (val >= 0) @divTrunc(val + half, d) else @divTrunc(val - half, d);
return .{ .value = rounded };
}
}
@@ -132,14 +219,135 @@ pub fn Scalar(comptime T: type, comptime d: Dimensions, comptime s: Scales) type
}
}
pub fn Vec(self: Self, comptime len: comptime_int) Vector(len, Self) {
// ---------------------------------------------------------------
// Comparisons
// ---------------------------------------------------------------
/// Compares two Scalar for exact equality.
/// Dimensions must match — compile error otherwise. Scales are auto-resolved.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn eq(self: Self, r: anytype) bool {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in eq: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime RhsType == Self)
return self.value == rhs_s.value;
const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value;
return lhs_val == rhs_val;
}
/// Compares two quantities for inequality.
/// Dimensions must match — compile error otherwise. Scales are auto-resolved.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn ne(self: Self, r: anytype) bool {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in ne: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime RhsType == Self)
return self.value != rhs_s.value;
const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value;
return lhs_val != rhs_val;
}
/// Returns true if this quantity is strictly greater than the right-hand side.
/// Dimensions must match — compile error otherwise. Scales are auto-resolved.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn gt(self: Self, r: anytype) bool {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in gt: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime RhsType == Self)
return self.value > rhs_s.value;
const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value;
return lhs_val > rhs_val;
}
/// Returns true if this quantity is greater than or equal to the right-hand side.
/// Dimensions must match — compile error otherwise. Scales are auto-resolved.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn gte(self: Self, r: anytype) bool {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in gte: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime RhsType == Self)
return self.value >= rhs_s.value;
const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value;
return lhs_val >= rhs_val;
}
/// Returns true if this quantity is strictly less than the right-hand side.
/// Dimensions must match — compile error otherwise. Scales are auto-resolved.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn lt(self: Self, r: anytype) bool {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in lt: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime RhsType == Self)
return self.value < rhs_s.value;
const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value;
return lhs_val < rhs_val;
}
/// Returns true if this quantity is less than or equal to the right-hand side.
/// Dimensions must match — compile error otherwise. Scales are auto-resolved.
/// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn lte(self: Self, r: anytype) bool {
const rhs_s = rhs(r);
const RhsType = @TypeOf(rhs_s);
if (comptime !dims.eql(RhsType.dims))
@compileError("Dimension mismatch in lte: " ++ dims.str() ++ " vs " ++ RhsType.dims.str());
if (comptime RhsType == Self)
return self.value <= rhs_s.value;
const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt());
const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value;
return lhs_val <= rhs_val;
}
// ---------------------------------------------------------------
// Vector helpers
// ---------------------------------------------------------------
/// Return a `Vector(len, Self)` type.
pub fn Vec(_: Self, comptime len: comptime_int) type {
return Vector(len, Self);
}
/// Return a `Vector(len, Self)` with all components set to this value.
pub fn vec(self: Self, comptime len: comptime_int) Vector(len, Self) {
return Vector(len, Self).initDefault(self.value);
}
/// Shorthand for `Vec(3)` — wrap this value into a 3-component vector.
pub fn vec3(self: Self) Vec3 {
return Vec3.initDefault(self.value);
}
// ---------------------------------------------------------------
// Formatting
// ---------------------------------------------------------------
pub fn formatNumber(
self: Self,
writer: *std.Io.Writer,
@@ -178,8 +386,8 @@ pub fn Scalar(comptime T: type, comptime d: Dimensions, comptime s: Scales) type
}
test "Generate quantity" {
const Meter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = -3 }));
const Second = Scalar(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{ .T = .n }));
const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = @enumFromInt(-3) });
const Second = Scalar(f32, .{ .T = 1 }, .{ .T = .n });
const distance = Meter{ .value = 10 };
const time = Second{ .value = 2 };
@@ -188,8 +396,34 @@ test "Generate quantity" {
try std.testing.expectEqual(2, time.value);
}
test "Comparisons (eq, ne, gt, gte, lt, lte)" {
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k });
const m1000 = Meter{ .value = 1000 };
const km1 = KiloMeter{ .value = 1 };
const km2 = KiloMeter{ .value = 2 };
// Equal / Not Equal
try std.testing.expect(m1000.eq(km1));
try std.testing.expect(km1.eq(m1000));
try std.testing.expect(km2.ne(m1000));
// Greater Than / Greater Than or Equal
try std.testing.expect(km2.gt(m1000));
try std.testing.expect(km2.gt(km1));
try std.testing.expect(km1.gte(m1000));
try std.testing.expect(km2.gte(m1000));
// Less Than / Less Than or Equal
try std.testing.expect(m1000.lt(km2));
try std.testing.expect(km1.lt(km2));
try std.testing.expect(km1.lte(m1000));
try std.testing.expect(m1000.lte(km2));
}
test "Add" {
const Meter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const distance = Meter{ .value = 10 };
const distance2 = Meter{ .value = 20 };
@@ -198,7 +432,7 @@ test "Add" {
try std.testing.expectEqual(30, added.value);
try std.testing.expectEqual(1, @TypeOf(added).dims.get(.L));
const KiloMeter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k });
const distance3 = KiloMeter{ .value = 2 };
const added2 = distance.add(distance3);
try std.testing.expectEqual(2010, added2.value);
@@ -208,7 +442,7 @@ test "Add" {
try std.testing.expectEqual(2, added3.value);
try std.testing.expectEqual(1, @TypeOf(added3).dims.get(.L));
const KiloMeter_f = Scalar(f64, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const KiloMeter_f = Scalar(f64, .{ .L = 1 }, .{ .L = .k });
const distance4 = KiloMeter_f{ .value = 2 };
const added4 = distance4.add(distance).to(KiloMeter_f);
try std.testing.expectApproxEqAbs(2.01, added4.value, 0.000001);
@@ -216,8 +450,8 @@ test "Add" {
}
test "Sub" {
const Meter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const KiloMeter_f = Scalar(f64, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const KiloMeter_f = Scalar(f64, .{ .L = 1 }, .{ .L = .k });
const a = Meter{ .value = 500 };
const b = Meter{ .value = 200 };
@@ -233,46 +467,46 @@ test "Sub" {
}
test "MulBy" {
const Meter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Scalar(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const Second = Scalar(f32, .{ .T = 1 }, .{});
const d = Meter{ .value = 3.0 };
const t = Second{ .value = 4.0 };
const area_time = d.mulBy(t);
const area_time = d.mul(t);
try std.testing.expectEqual(12, area_time.value);
try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.L));
try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.T));
const d2 = Meter{ .value = 5.0 };
const area = d.mulBy(d2);
const area = d.mul(d2);
try std.testing.expectEqual(15, area.value);
try std.testing.expectEqual(2, @TypeOf(area).dims.get(.L));
try std.testing.expectEqual(0, @TypeOf(area).dims.get(.T));
}
test "MulBy with scale" {
const KiloMeter = Scalar(f32, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const KiloGram = Scalar(f32, Dimensions.init(.{ .M = 1 }), Scales.init(.{ .M = .k }));
const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k });
const KiloGram = Scalar(f32, .{ .M = 1 }, .{ .M = .k });
const dist = KiloMeter{ .value = 2.0 };
const mass = KiloGram{ .value = 3.0 };
const prod = dist.mulBy(mass);
const prod = dist.mul(mass);
try std.testing.expectEqual(1, @TypeOf(prod).dims.get(.L));
try std.testing.expectEqual(1, @TypeOf(prod).dims.get(.M));
}
test "MulBy with type change" {
const Meter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const Second = Scalar(f64, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const KmSec = Scalar(i64, Dimensions.init(.{ .L = 1, .T = 1 }), Scales.init(.{ .L = .k }));
const KmSec_f = Scalar(f32, Dimensions.init(.{ .L = 1, .T = 1 }), Scales.init(.{ .L = .k }));
const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = .k });
const Second = Scalar(f64, .{ .T = 1 }, .{});
const KmSec = Scalar(i64, .{ .L = 1, .T = 1 }, .{ .L = .k });
const KmSec_f = Scalar(f32, .{ .L = 1, .T = 1 }, .{ .L = .k });
const d = Meter{ .value = 3.0 };
const t = Second{ .value = 4.0 };
const area_time = d.mulBy(t).to(KmSec);
const area_time_f = d.mulBy(t).to(KmSec_f);
const area_time = d.mul(t).to(KmSec);
const area_time_f = d.mul(t).to(KmSec_f);
try std.testing.expectEqual(12, area_time.value);
try std.testing.expectApproxEqAbs(12, area_time_f.value, 0.0001);
try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.L));
@@ -280,67 +514,95 @@ test "MulBy with type change" {
}
test "MulBy small" {
const Meter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .n }));
const Second = Scalar(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = .n });
const Second = Scalar(f32, .{ .T = 1 }, .{});
const d = Meter{ .value = 3.0 };
const t = Second{ .value = 4.0 };
const area_time = d.mulBy(t);
const area_time = d.mul(t);
try std.testing.expectEqual(12, area_time.value);
try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.L));
try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.T));
}
test "Scale" {
const Meter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Scalar(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
test "MulBy dimensionless" {
const DimLess = Scalar(i128, .{}, .{});
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const d = Meter{ .value = 7 };
const scaled = d.scale(3);
const scaled = d.mul(DimLess{ .value = 3 });
try std.testing.expectEqual(21, scaled.value);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
}
const t = Second{ .value = 1.5 };
const scaled_f = t.scale(4.0);
try std.testing.expectApproxEqAbs(@as(f32, 6.0), scaled_f.value, 1e-4);
test "Sqrt" {
const MeterSquare = Scalar(i128, .{ .L = 2 }, .{});
var d = MeterSquare{ .value = 9 };
var scaled = d.sqrt();
try std.testing.expectEqual(3, scaled.value);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
d = MeterSquare{ .value = -5 };
scaled = d.sqrt();
try std.testing.expectEqual(0, scaled.value);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
const MeterSquare_f = Scalar(f64, .{ .L = 2 }, .{});
const d2 = MeterSquare_f{ .value = 20 };
const scaled2 = d2.sqrt();
try std.testing.expectApproxEqAbs(4.472135955, scaled2.value, 1e-4);
try std.testing.expectEqual(1, @TypeOf(scaled2).dims.get(.L));
}
test "Chained: velocity and acceleration" {
const Meter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Scalar(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const Second = Scalar(f32, .{ .T = 1 }, .{});
const dist = Meter{ .value = 100.0 };
const t1 = Second{ .value = 5.0 };
const velocity = dist.divBy(t1);
const velocity = dist.div(t1);
try std.testing.expectEqual(20, velocity.value);
try std.testing.expectEqual(1, @TypeOf(velocity).dims.get(.L));
try std.testing.expectEqual(-1, @TypeOf(velocity).dims.get(.T));
const t2 = Second{ .value = 4.0 };
const accel = velocity.divBy(t2);
const accel = velocity.div(t2);
try std.testing.expectEqual(5, accel.value);
try std.testing.expectEqual(1, @TypeOf(accel).dims.get(.L));
try std.testing.expectEqual(-2, @TypeOf(accel).dims.get(.T));
}
test "DivBy integer exact" {
const Meter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Scalar(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const Second = Scalar(f32, .{ .T = 1 }, .{});
const dist = Meter{ .value = 120 };
const time = Second{ .value = 4 };
const vel = dist.divBy(time);
const vel = dist.div(time);
try std.testing.expectEqual(30, vel.value);
try std.testing.expectEqual(1, @TypeOf(vel).dims.get(.L));
try std.testing.expectEqual(-1, @TypeOf(vel).dims.get(.T));
}
test "Finer scales skip dim 0" {
const Dimless = Scalar(i128, .{}, .{});
const KiloMetre = Scalar(i128, .{ .L = 1 }, .{ .L = .k });
const r = Dimless{ .value = 30 };
const time = KiloMetre{ .value = 4 };
const vel = r.mul(time);
try std.testing.expectEqual(120, vel.value);
try std.testing.expectEqual(Scales.UnitScale.k, @TypeOf(vel).scales.get(.L));
}
test "Conversion chain: km -> m -> cm" {
const KiloMeter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const Meter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const CentiMeter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .c }));
const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k });
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const CentiMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .c });
const km = KiloMeter{ .value = 15 };
const m = km.to(Meter);
@@ -351,9 +613,9 @@ test "Conversion chain: km -> m -> cm" {
}
test "Conversion: hours -> minutes -> seconds" {
const Hour = Scalar(i128, Dimensions.init(.{ .T = 1 }), Scales.init(.{ .T = .hour }));
const Minute = Scalar(i128, Dimensions.init(.{ .T = 1 }), Scales.init(.{ .T = .min }));
const Second = Scalar(i128, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const Hour = Scalar(i128, .{ .T = 1 }, .{ .T = .hour });
const Minute = Scalar(i128, .{ .T = 1 }, .{ .T = .min });
const Second = Scalar(i128, .{ .T = 1 }, .{});
const h = Hour{ .value = 1.0 };
const min = h.to(Minute);
@@ -364,7 +626,7 @@ test "Conversion: hours -> minutes -> seconds" {
}
test "Negative values" {
const Meter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const a = Meter{ .value = 5 };
const b = Meter{ .value = 20 };
@@ -373,17 +635,9 @@ test "Negative values" {
}
test "Format Scalar" {
const MeterPerSecondSq = Scalar(
f32,
Dimensions.init(.{ .L = 1, .T = -2 }),
Scales.init(.{ .T = .n }),
);
const KgMeterPerSecond = Scalar(
f32,
Dimensions.init(.{ .M = 1, .L = 1, .T = -1 }),
Scales.init(.{ .M = .k }),
);
const Meter = Scalar(f32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const MeterPerSecondSq = Scalar(f32, .{ .L = 1, .T = -2 }, .{ .T = .n });
const KgMeterPerSecond = Scalar(f32, .{ .M = 1, .L = 1, .T = -1 }, .{ .M = .k });
const Meter = Scalar(f32, .{ .L = 1 }, .{});
const m = Meter{ .value = 1.23456 };
const accel = MeterPerSecondSq{ .value = 9.81 };
@@ -402,3 +656,162 @@ test "Format Scalar" {
res = try std.fmt.bufPrint(&buf, "{d:_>10.1}", .{m});
try std.testing.expectEqualStrings("_______1.2m", res);
}
test "Abs" {
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const m1 = Meter{ .value = -50 };
const m2 = m1.abs();
try std.testing.expectEqual(50, m2.value);
try std.testing.expectEqual(1, @TypeOf(m2).dims.get(.L));
const m_float = Scalar(f32, .{ .L = 1 }, .{});
const m3 = m_float{ .value = -42.5 };
try std.testing.expectEqual(42.5, m3.abs().value);
}
test "Pow" {
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const d = Meter{ .value = 4 };
const area = d.pow(2);
try std.testing.expectEqual(16, area.value);
try std.testing.expectEqual(2, @TypeOf(area).dims.get(.L));
const volume = d.pow(3);
try std.testing.expectEqual(64, volume.value);
try std.testing.expectEqual(3, @TypeOf(volume).dims.get(.L));
// Float test
const MeterF = Scalar(f32, .{ .L = 1 }, .{});
const d_f = MeterF{ .value = 2.0 };
const area_f = d_f.pow(3);
try std.testing.expectEqual(8.0, area_f.value);
try std.testing.expectEqual(3, @TypeOf(area_f).dims.get(.L));
}
test "mul comptime_int" {
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const d = Meter{ .value = 7 };
const scaled = d.mul(3); // comptime_int → dimensionless
try std.testing.expectEqual(21, scaled.value);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
try std.testing.expectEqual(0, @TypeOf(scaled).dims.get(.T));
}
test "mul comptime_float" {
const MeterF = Scalar(f64, .{ .L = 1 }, .{});
const d = MeterF{ .value = 4.0 };
const scaled = d.mul(2.5); // comptime_float → dimensionless
try std.testing.expectApproxEqAbs(10.0, scaled.value, 1e-9);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
}
test "mul T (value type)" {
const MeterF = Scalar(f32, .{ .L = 1 }, .{});
const d = MeterF{ .value = 6.0 };
const factor: f32 = 0.5;
const scaled = d.mul(factor); // bare f32 → dimensionless
try std.testing.expectApproxEqAbs(3.0, scaled.value, 1e-6);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
}
test "div comptime_int" {
const Meter = Scalar(i128, .{ .L = 1 }, .{});
const d = Meter{ .value = 100 };
const half = d.div(4); // comptime_int → dimensionless divisor
try std.testing.expectEqual(25, half.value);
try std.testing.expectEqual(1, @TypeOf(half).dims.get(.L));
}
test "div comptime_float" {
const MeterF = Scalar(f64, .{ .L = 1 }, .{});
const d = MeterF{ .value = 9.0 };
const r = d.div(3.0);
try std.testing.expectApproxEqAbs(3.0, r.value, 1e-9);
try std.testing.expectEqual(1, @TypeOf(r).dims.get(.L));
}
test "add/sub bare number on dimensionless scalar" {
// Bare numbers are dimensionless, so add/sub only works when Self is also dimensionless.
const DimLess = Scalar(i128, .{}, .{});
const a = DimLess{ .value = 10 };
const b = a.add(5); // comptime_int, both dimensionless → ok
try std.testing.expectEqual(15, b.value);
const c = a.sub(3);
try std.testing.expectEqual(7, c.value);
}
test "Imperial length scales" {
const Foot = Scalar(f64, .{ .L = 1 }, .{ .L = .ft });
const Meter = Scalar(f64, .{ .L = 1 }, .{});
const Inch = Scalar(f64, .{ .L = 1 }, .{ .L = .inch });
const CentiMeter = Scalar(f64, .{ .L = 1 }, .{ .L = .c });
const Mile = Scalar(f64, .{ .L = 1 }, .{ .L = .mi });
const KiloMeter = Scalar(f64, .{ .L = 1 }, .{ .L = .k });
const Yard = Scalar(f64, .{ .L = 1 }, .{ .L = .yd });
// 1 ft → 0.3048 m
const one_ft = Foot{ .value = 1.0 };
try std.testing.expectApproxEqAbs(0.3048, one_ft.to(Meter).value, 1e-9);
// 12 in → 1 ft
const twelve_in = Inch{ .value = 12.0 };
try std.testing.expectApproxEqAbs(1.0, twelve_in.to(Foot).value, 1e-9);
// 1 in → 2.54 cm
const one_in = Inch{ .value = 1.0 };
try std.testing.expectApproxEqAbs(2.54, one_in.to(CentiMeter).value, 1e-9);
// 1 mi → 1.609344 km
const one_mi = Mile{ .value = 1.0 };
try std.testing.expectApproxEqAbs(1.609344, one_mi.to(KiloMeter).value, 1e-9);
// 3 ft → 1 yd
const three_ft = Foot{ .value = 3.0 };
try std.testing.expectApproxEqAbs(1.0, three_ft.to(Yard).value, 1e-9);
}
test "Imperial mass scales" {
const Pound = Scalar(f64, .{ .M = 1 }, .{ .M = .lb });
const KiloGram = Scalar(f64, .{ .M = 1 }, .{ .M = .k });
const Ounce = Scalar(f64, .{ .M = 1 }, .{ .M = .oz });
const Stone = Scalar(f64, .{ .M = 1 }, .{ .M = .st });
// 1 lb → ~0.453592 kg
const one_lb = Pound{ .value = 1.0 };
try std.testing.expectApproxEqAbs(0.45359237, one_lb.to(KiloGram).value, 1e-6);
// 16 oz → 1 lb
const sixteen_oz = Ounce{ .value = 16.0 };
try std.testing.expectApproxEqAbs(1.0, sixteen_oz.to(Pound).value, 1e-6);
// 1 stone → 14 lb
const one_st = Stone{ .value = 1.0 };
try std.testing.expectApproxEqAbs(14.0, one_st.to(Pound).value, 1e-4);
// 2 lb + 8 oz → 2.5 lb
const two_lb = Pound{ .value = 2.0 };
const eight_oz = Ounce{ .value = 8.0 };
const total = two_lb.add(eight_oz).to(Pound);
try std.testing.expectApproxEqAbs(2.5, total.value, 1e-6);
}
test "comparisons with comptime_int on dimensionless scalar" {
const DimLess = Scalar(i128, .{}, .{});
const x = DimLess{ .value = 42 };
try std.testing.expect(x.eq(42));
try std.testing.expect(x.ne(0));
try std.testing.expect(x.gt(10));
try std.testing.expect(x.gte(42));
try std.testing.expect(x.lt(100));
try std.testing.expect(x.lte(42));
}
+68 -29
View File
@@ -3,6 +3,22 @@ const hlp = @import("helper.zig");
const Dimensions = @import("Dimensions.zig");
const Dimension = @import("Dimensions.zig").Dimension;
// TODO: add more scales like feet and inch
/// Use to initiate Scalar and Scales type
pub const ArgOpts = struct {
L: UnitScale = .none,
M: UnitScale = .none,
T: UnitScale = .none,
I: UnitScale = .none,
Tp: UnitScale = .none,
N: UnitScale = .none,
J: UnitScale = .none,
};
/// SI prefix (pico…peta) plus time-unit aliases (min, hour, year).
/// The integer value encodes the exponent for SI prefixes (e.g. `k = 3` → 10³),
/// and the literal factor for time units (e.g. `hour = 3600`).
pub const UnitScale = enum(isize) {
P = 15,
T = 12,
@@ -25,6 +41,18 @@ pub const UnitScale = enum(isize) {
hour = 3_600,
year = 31_536_000,
// Imperial Length (Literal factors in meters)
// 1 inch = 0.0254 meters. Since enum backing is isize,
// we use a unique tag and handle the float in getFactor.
inch = -1001,
ft = -1002,
yd = -1003,
mi = -1004,
oz = -1005, // 1 oz = 28.3495231 g
lb = -1006, // 1 lb = 453.59237 g (= 16 oz)
st = -1007, // 1 stone = 6350.29318 g (= 14 lb)
// Undefined
_,
@@ -32,34 +60,44 @@ pub const UnitScale = enum(isize) {
var buf: [16]u8 = undefined;
return switch (self) {
inline .none => "",
inline .P, .T, .G, .M, .k, .h, .da, .d, .c, .m, .u, .n, .p, .f, .min, .hour, .year => @tagName(self),
inline .P, .T, .G, .M, .k, .h, .da, .d, .c, .m, .u, .n, .p, .f, .min, .hour, .year, .inch, .ft, .yd, .mi, .oz, .lb, .st => @tagName(self),
else => std.fmt.bufPrint(&buf, "[{d}]", .{@intFromEnum(self)}) catch "[]", // This cannot be inline because of non exhaustive enum, but that's ok, it is just str, not calculation
};
}
/// Helper to get the actual scaling factor
pub inline fn getFactor(self: @This()) comptime_float {
return switch (self) {
return comptime switch (self) {
// Standard SI Exponents
inline .P, .T, .G, .M, .k, .h, .da, .none, .d, .c, .m, .u, .n, .p, .f => std.math.pow(f64, 10.0, @floatFromInt(@intFromEnum(self))),
inline else => @floatFromInt(@intFromEnum(self)),
};
}
/// Helper to get the actual scaling factor in i32
pub inline fn getFactorInt(self: @This()) comptime_int {
return switch (self) {
inline .P, .T, .G, .M, .k, .h, .da, .none, .d, .c, .m, .u, .n, .p, .f => comptime std.math.powi(i32, 10.0, @intFromEnum(self)) catch 0,
inline else => comptime @intFromEnum(self),
// Time Factors
inline .min, .hour, .year => @floatFromInt(@intFromEnum(self)),
// Imperial Length (metres)
inline .inch => 0.0254,
inline .ft => 0.3048,
inline .yd => 0.9144,
inline .mi => 1609.344,
// Imperial Mass (grams — base unit for M is gram, i.e. .none = 1 g)
inline .oz => 28.3495231,
inline .lb => 453.59237,
inline .st => 6350.29318,
inline else => @floatFromInt(@intFromEnum(self)),
};
}
};
const Scales = @This();
/// Maps each SI base dimension to its `UnitScale`. Stored and resolved entirely at comptime.
const Self = @This();
data: std.EnumArray(Dimension, UnitScale),
pub fn init(comptime init_val: anytype) Scales {
comptime var s = Scales{ .data = std.EnumArray(Dimension, UnitScale).initFill(.none) };
/// Create a `Scales` from a struct literal, e.g. `.{ .L = .k, .T = .hour }`.
/// Unspecified dimensions default to `.none` (factor 1).
pub fn init(comptime init_val: ArgOpts) Self {
comptime var s = Self{ .data = std.EnumArray(Dimension, UnitScale).initFill(.none) };
inline for (std.meta.fields(@TypeOf(init_val))) |f| {
if (comptime hlp.isInt(@TypeOf(@field(init_val, f.name))))
s.data.set(@field(Dimension, f.name), @enumFromInt(@field(init_val, f.name)))
@@ -69,35 +107,36 @@ pub fn init(comptime init_val: anytype) Scales {
return s;
}
pub fn initFill(comptime val: UnitScale) Scales {
pub fn initFill(comptime val: UnitScale) Self {
return comptime .{ .data = std.EnumArray(Dimension, UnitScale).initFill(val) };
}
pub fn get(comptime self: Scales, comptime key: Dimension) UnitScale {
pub fn get(comptime self: Self, comptime key: Dimension) UnitScale {
return comptime self.data.get(key);
}
pub fn set(comptime self: *Scales, comptime key: Dimension, comptime val: UnitScale) void {
pub fn set(comptime self: *Self, comptime key: Dimension, comptime val: UnitScale) void {
comptime self.data.set(key, val);
}
pub fn min(comptime s1: Scales, comptime s2: Scales) Scales {
comptime var out = Scales.initFill(.none);
inline for (std.enums.values(Dimension)) |dim|
out.set(dim, if (s1.get(dim).getFactorInt() > s2.get(dim).getFactorInt()) s2.get(dim) else s1.get(dim));
return out;
pub fn argsOpt(self: Self) ArgOpts {
var args: ArgOpts = undefined;
inline for (std.enums.values(Dimension)) |d|
@field(args, @tagName(d)) = self.get(d);
return args;
}
pub inline fn getFactor(comptime s: Scales, comptime d: Dimensions) comptime_float {
comptime var factor: f64 = 1.0;
inline for (std.enums.values(Dimension)) |dim| {
const power = d.get(dim);
/// Compute the combined scale factor for a given dimension signature.
/// Each dimension's prefix is raised to its exponent and multiplied together.
pub inline fn getFactor(comptime s: Self, comptime d: Dimensions) comptime_float {
var factor: f64 = 1.0;
for (std.enums.values(Dimension)) |dim| {
const power = comptime d.get(dim);
if (power == 0) continue;
const base = s.get(dim).getFactor();
var i: i32 = 0;
var i: comptime_int = 0;
const abs_power = if (power < 0) -power else power;
while (i < abs_power) : (i += 1) {
if (power > 0)
@@ -106,5 +145,5 @@ pub inline fn getFactor(comptime s: Scales, comptime d: Dimensions) comptime_flo
factor /= base;
}
}
return factor;
return comptime factor;
}
+579 -81
View File
@@ -7,10 +7,9 @@ const UnitScale = Scales.UnitScale;
const Dimensions = @import("Dimensions.zig");
const Dimension = Dimensions.Dimension;
/// A fixed-size array of `len` elements sharing the same dimension and scale as scalar type `Q`.
pub fn Vector(comptime len: usize, comptime Q: type) type {
const T = Q.ValueType;
const d: Dimensions = Q.dims;
const s: Scales = Q.scales;
return struct {
data: [len]T,
@@ -18,128 +17,326 @@ pub fn Vector(comptime len: usize, comptime Q: type) type {
const Self = @This();
pub const ScalarType = Q;
pub const ValueType = T;
pub const dims: Dimensions = d;
pub const scales = s;
pub const dims: Dimensions = Q.dims;
pub const scales = Q.scales;
pub const zero = initDefault(0);
pub const one = initDefault(1);
pub fn initDefault(v: T) Self {
var data: [len]T = undefined;
for (&data) |*item| item.* = v;
inline for (&data) |*item| item.* = v;
return .{ .data = data };
}
pub fn add(self: Self, rhs: anytype) Vector(len, Scalar(T, d, s.min(@TypeOf(rhs).scales))) {
// -------------------------------------------------------------------
// Internal: scalar-rhs normalisation (mirrors Scalar.zig)
// -------------------------------------------------------------------
/// Resolved Scalar type for a scalar operand (bare number or Scalar).
/// Passing another Vector here is a compile error.
inline fn ScalarRhsT(comptime Rhs: type) type {
if (comptime switch (@typeInfo(Rhs)) {
.@"struct", .@"enum", .@"union", .@"opaque" => @hasDecl(Rhs, "ScalarType"),
else => false,
})
@compileError(
"Expected a Scalar or bare number; got a Vector. " ++
"Use mul / div for element-wise vector operations.",
);
return hlp.rhsScalarType(T, Rhs);
}
/// Normalise a scalar rhs (bare number → dimensionless Scalar).
inline fn scalarRhs(r: anytype) ScalarRhsT(@TypeOf(r)) {
return hlp.toRhsScalar(T, r);
}
// -------------------------------------------------------------------
// Vector–Vector operations (rhs must be a Vector of the same length)
// -------------------------------------------------------------------
/// Element-wise addition. Dimensions must match; scales resolve to the finer of the two.
pub inline fn add(self: Self, rhs: anytype) Vector(len, Scalar(
T,
dims.argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) {
const Tr = @TypeOf(rhs);
var res: Vector(len, Scalar(T, d, s.min(Tr.scales))) = undefined;
for (self.data, 0..) |v, i| {
var res: Vector(len, Scalar(
T,
dims.argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) = undefined;
inline for (self.data, 0..) |v, i| {
const q = (Q{ .value = v }).add(Tr.ScalarType{ .value = rhs.data[i] });
res.data[i] = q.value;
}
return res;
}
pub fn sub(self: Self, rhs: anytype) Vector(len, Scalar(T, d, s.min(@TypeOf(rhs).scales))) {
/// Element-wise subtraction. Dimensions must match; scales resolve to the finer of the two.
pub inline fn sub(self: Self, rhs: anytype) Vector(len, Scalar(
T,
dims.argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) {
const Tr = @TypeOf(rhs);
var res: Vector(len, Scalar(T, d, s.min(Tr.scales))) = undefined;
for (self.data, 0..) |v, i| {
var res: Vector(len, Scalar(
T,
dims.argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) = undefined;
inline for (self.data, 0..) |v, i| {
const q = (Q{ .value = v }).sub(Tr.ScalarType{ .value = rhs.data[i] });
res.data[i] = q.value;
}
return res;
}
pub fn divBy(
/// Element-wise division. Dimension exponents are subtracted per component.
pub inline fn div(
self: Self,
rhs: anytype,
) Vector(len, Scalar(T, d.sub(@TypeOf(rhs).dims), s.min(@TypeOf(rhs).scales))) {
) Vector(len, Scalar(
T,
dims.sub(@TypeOf(rhs).dims).argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) {
const Tr = @TypeOf(rhs);
var res: Vector(len, Scalar(T, d.sub(Tr.dims), s.min(Tr.scales))) = undefined;
for (self.data, 0..) |v, i| {
const q = (Q{ .value = v }).divBy(Tr.ScalarType{ .value = rhs.data[i] });
var res: Vector(len, Scalar(
T,
dims.sub(Tr.dims).argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) = undefined;
inline for (self.data, 0..) |v, i| {
const q = (Q{ .value = v }).div(Tr.ScalarType{ .value = rhs.data[i] });
res.data[i] = q.value;
}
return res;
}
pub fn mulBy(
/// Element-wise multiplication. Dimension exponents are summed per component.
pub inline fn mul(
self: Self,
rhs: anytype,
) Vector(len, Scalar(T, d.add(@TypeOf(rhs).dims), s.min(@TypeOf(rhs).scales))) {
) Vector(len, Scalar(
T,
dims.add(@TypeOf(rhs).dims).argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) {
const Tr = @TypeOf(rhs);
var res: Vector(len, Scalar(T, d.add(Tr.dims), s.min(Tr.scales))) = undefined;
for (self.data, 0..) |v, i| {
const q = (Q{ .value = v }).mulBy(Tr.ScalarType{ .value = rhs.data[i] });
var res: Vector(len, Scalar(
T,
dims.add(Tr.dims).argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) = undefined;
inline for (self.data, 0..) |v, i| {
const q = (Q{ .value = v }).mul(Tr.ScalarType{ .value = rhs.data[i] });
res.data[i] = q.value;
}
return res;
}
pub fn divByScalar(
// -------------------------------------------------------------------
// Vector–Scalar operations
// scalar may be: Scalar, T, comptime_int, comptime_float
// -------------------------------------------------------------------
/// Divide every component by a single scalar. Dimensions are subtracted.
/// `scalar` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn divScalar(
self: Self,
scalar: anytype,
) Vector(len, Scalar(T, d.sub(@TypeOf(scalar).dims), s.min(@TypeOf(scalar).scales))) {
var res: Vector(len, Scalar(T, d.sub(@TypeOf(scalar).dims), s.min(@TypeOf(scalar).scales))) = undefined;
for (self.data, 0..) |v, i| {
) Vector(len, Scalar(
T,
dims.sub(ScalarRhsT(@TypeOf(scalar)).dims).argsOpt(),
hlp.finerScales(Self, ScalarRhsT(@TypeOf(scalar))).argsOpt(),
)) {
const s_norm = scalarRhs(scalar);
const SN = @TypeOf(s_norm);
var res: Vector(len, Scalar(
T,
dims.sub(SN.dims).argsOpt(),
hlp.finerScales(Self, SN).argsOpt(),
)) = undefined;
inline for (self.data, 0..) |v, i|
res.data[i] = (Q{ .value = v }).div(s_norm).value;
return res;
}
/// Multiply every component by a single scalar. Dimensions are summed.
/// `scalar` may be a Scalar, `T`, `comptime_int`, or `comptime_float`.
pub inline fn mulScalar(
self: Self,
scalar: anytype,
) Vector(len, Scalar(
T,
dims.add(ScalarRhsT(@TypeOf(scalar)).dims).argsOpt(),
hlp.finerScales(Self, ScalarRhsT(@TypeOf(scalar))).argsOpt(),
)) {
const s_norm = scalarRhs(scalar);
const SN = @TypeOf(s_norm);
var res: Vector(len, Scalar(
T,
dims.add(SN.dims).argsOpt(),
hlp.finerScales(Self, SN).argsOpt(),
)) = undefined;
inline for (self.data, 0..) |v, i|
res.data[i] = (Q{ .value = v }).mul(s_norm).value;
return res;
}
// -------------------------------------------------------------------
// Dot / Cross
// -------------------------------------------------------------------
/// Standard dot product. Dimensions are summed (e.g., Force * Distance = Energy).
/// Returns a Scalar type with the combined dimensions and finest scale.
pub inline fn dot(self: Self, rhs: anytype) Scalar(
T,
dims.add(@TypeOf(rhs).dims).argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
) {
const Tr = @TypeOf(rhs);
var sum: T = 0;
inline for (self.data, 0..) |v, i| {
const q_lhs = Q{ .value = v };
const q_rhs = Tr.ScalarType{ .value = rhs.data[i] };
sum += q_lhs.mul(q_rhs).value;
}
return .{ .value = sum };
}
/// 3D Cross product. Dimensions are summed.
/// Only valid for vectors of length 3.
pub inline fn cross(self: Self, rhs: anytype) Vector(3, Scalar(
T,
dims.add(@TypeOf(rhs).dims).argsOpt(),
hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(),
)) {
if (comptime len != 3)
@compileError("Cross product is only defined for Vector(3, ...)");
const Tr = @TypeOf(rhs);
const ResScalar = Scalar(T, dims.add(Tr.dims).argsOpt(), hlp.finerScales(Self, Tr).argsOpt());
const ResVec = Vector(3, ResScalar);
// Calculation: [y1*z2 - z1*y2, z1*x2 - x1*z2, x1*y2 - y1*x2]
const s1 = Q{ .value = self.data[0] };
const s2 = Q{ .value = self.data[1] };
const s3 = Q{ .value = self.data[2] };
const o1 = Tr.ScalarType{ .value = rhs.data[0] };
const o2 = Tr.ScalarType{ .value = rhs.data[1] };
const o3 = Tr.ScalarType{ .value = rhs.data[2] };
return ResVec{
.data = .{
s2.mul(o3).sub(s3.mul(o2)).value,
s3.mul(o1).sub(s1.mul(o3)).value,
s1.mul(o2).sub(s2.mul(o1)).value,
},
};
}
// -------------------------------------------------------------------
// Unary
// -------------------------------------------------------------------
/// Returns a vector where each component is the absolute value of the original.
pub inline fn abs(self: Self) Self {
var res: Self = undefined;
inline for (self.data, 0..) |v, i| {
const q = Q{ .value = v };
res.data[i] = q.divBy(scalar).value;
res.data[i] = q.abs().value;
}
return res;
}
pub fn mulByScalar(
self: Self,
scalar: anytype,
) Vector(len, Scalar(T, d.add(@TypeOf(scalar).dims), s.min(@TypeOf(scalar).scales))) {
var res: Vector(len, Scalar(T, d.add(@TypeOf(scalar).dims), s.min(@TypeOf(scalar).scales))) = undefined;
for (self.data, 0..) |v, i| {
/// Returns a vector where each component is the absolute value of the original.
pub inline fn sqrt(self: Self) Self {
var res: Self = undefined;
inline for (self.data, 0..) |v, i| {
const q = Q{ .value = v };
res.data[i] = q.mulBy(scalar).value;
res.data[i] = q.sqrt().value;
}
return res;
}
/// Multiplies all components of the vector together.
/// Resulting dimensions are (Original Dims * len).
pub inline fn product(self: Self) Scalar(
T,
dims.scale(len).argsOpt(),
scales.argsOpt(),
) {
var res_val: T = 1;
if (comptime hlp.isInt(T)) {
inline for (self.data) |v|
res_val = res_val *| v;
} else inline for (self.data) |v|
res_val *= v;
return .{ .value = res_val };
}
/// Raises every component to a compile-time integer power.
/// Dimensions are scaled by the exponent.
pub inline fn pow(self: Self, comptime exp: comptime_int) Vector(
len,
Scalar(
T,
dims.scale(exp).argsOpt(),
scales.argsOpt(),
),
) {
const ResScalar = Scalar(T, dims.scale(exp).argsOpt(), scales.argsOpt());
var res: Vector(len, ResScalar) = undefined;
inline for (self.data, 0..) |v, i| {
const q = Q{ .value = v };
res.data[i] = q.pow(exp).value;
}
return res;
}
/// Negate all components. Dimensions are preserved.
pub fn negate(self: Self) Self {
var res: Self = undefined;
for (self.data, 0..) |v, i| {
inline for (self.data, 0..) |v, i|
res.data[i] = -v;
}
return res;
}
pub fn scale(self: Self, rhs: T) Self {
var res: Self = undefined;
for (self.data, 0..) |v, i| {
res.data[i] = (Q{ .value = v }).scale(rhs).value;
}
return res;
}
// -------------------------------------------------------------------
// Conversion
// -------------------------------------------------------------------
pub fn to(self: Self, comptime DestQ: type) Vector(len, DestQ) {
/// Convert all components to a compatible scalar type. Compile error on dimension mismatch.
pub inline fn to(self: Self, comptime DestQ: type) Vector(len, DestQ) {
var res: Vector(len, DestQ) = undefined;
for (self.data, 0..) |v, i| {
inline for (self.data, 0..) |v, i|
res.data[i] = (Q{ .value = v }).to(DestQ).value;
}
return res;
}
pub fn lengthSqr(self: Self) T {
// -------------------------------------------------------------------
// Length
// -------------------------------------------------------------------
/// Sum of squared components. Cheaper than `length` — use for comparisons.
pub inline fn lengthSqr(self: Self) T {
var sum: T = 0;
for (self.data) |v| {
inline for (self.data) |v|
sum += v * v;
}
return sum;
}
pub fn length(self: Self) T {
/// Euclidean length. Integer types use integer sqrt (truncated).
pub inline fn length(self: Self) T {
const len_sq = self.lengthSqr();
if (comptime @typeInfo(T) == .int) {
// Construct the unsigned equivalent of T at comptime (e.g., i32 -> u32)
const UnsignedT = @Int(.unsigned, @typeInfo(T).int.bits);
// len_sq is always positive, so @intCast is perfectly safe
const u_len_sq = @as(UnsignedT, @intCast(len_sq));
return @as(T, @intCast(std.math.sqrt(u_len_sq)));
} else {
@@ -147,6 +344,146 @@ pub fn Vector(comptime len: usize, comptime Q: type) type {
}
}
// -------------------------------------------------------------------
// Vector–Vector comparisons
// -------------------------------------------------------------------
/// Returns true only if all components are equal after scale resolution.
pub inline fn eqAll(self: Self, rhs: anytype) bool {
const Tr = @TypeOf(rhs);
if (comptime !dims.eql(Tr.dims))
@compileError("Dimension mismatch in eq: " ++ dims.str() ++ " vs " ++ Tr.dims.str());
inline for (self.data, 0..) |v, i| {
const lhs_q = Q{ .value = v };
const rhs_q = Tr.ScalarType{ .value = rhs.data[i] };
if (!lhs_q.eq(rhs_q)) return false;
}
return true;
}
/// Returns true if any component differs after scale resolution.
pub inline fn neAll(self: Self, rhs: anytype) bool {
return !self.eqAll(rhs);
}
/// Element-wise "Equal". Returns an array of booleans.
pub inline fn eq(self: Self, rhs: anytype) [len]bool {
const Tr = @TypeOf(rhs);
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).eq(Tr.ScalarType{ .value = rhs.data[i] });
return res;
}
/// Element-wise "Not Equal". Returns an array of booleans.
pub inline fn ne(self: Self, rhs: anytype) [len]bool {
const Tr = @TypeOf(rhs);
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).ne(Tr.ScalarType{ .value = rhs.data[i] });
return res;
}
/// Element-wise "Greater Than". Returns an array of booleans.
pub inline fn gt(self: Self, rhs: anytype) [len]bool {
const Tr = @TypeOf(rhs);
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).gt(Tr.ScalarType{ .value = rhs.data[i] });
return res;
}
/// Element-wise "Greater Than or Equal". Returns an array of booleans.
pub inline fn gte(self: Self, rhs: anytype) [len]bool {
const Tr = @TypeOf(rhs);
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).gte(Tr.ScalarType{ .value = rhs.data[i] });
return res;
}
/// Element-wise "Less Than". Returns an array of booleans.
pub inline fn lt(self: Self, rhs: anytype) [len]bool {
const Tr = @TypeOf(rhs);
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).lt(Tr.ScalarType{ .value = rhs.data[i] });
return res;
}
/// Element-wise "Less Than or Equal". Returns an array of booleans.
pub inline fn lte(self: Self, rhs: anytype) [len]bool {
const Tr = @TypeOf(rhs);
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).lte(Tr.ScalarType{ .value = rhs.data[i] });
return res;
}
// -------------------------------------------------------------------
// Vector–Scalar comparisons
// scalar may be: Scalar, T, comptime_int, comptime_float
// -------------------------------------------------------------------
/// Compares every element in the vector to a single scalar for equality.
/// Returns an array of booleans [len]bool. Dimensions must match; scales are auto-resolved.
pub inline fn eqScalar(self: Self, scalar: anytype) [len]bool {
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).eq(scalar);
return res;
}
/// Compares every element in the vector to a single scalar for inequality.
/// Returns an array of booleans [len]bool. Dimensions must match; scales are auto-resolved.
pub inline fn neScalar(self: Self, scalar: anytype) [len]bool {
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).ne(scalar);
return res;
}
/// Checks if each element in the vector is strictly greater than the given scalar.
/// Returns an array of booleans [len]bool.
pub inline fn gtScalar(self: Self, scalar: anytype) [len]bool {
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).gt(scalar);
return res;
}
/// Checks if each element in the vector is greater than or equal to the given scalar.
/// Returns an array of booleans [len]bool.
pub inline fn gteScalar(self: Self, scalar: anytype) [len]bool {
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).gte(scalar);
return res;
}
/// Checks if each element in the vector is strictly less than the given scalar.
/// Returns an array of booleans [len]bool.
pub inline fn ltScalar(self: Self, scalar: anytype) [len]bool {
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).lt(scalar);
return res;
}
/// Checks if each element in the vector is less than or equal to the given scalar.
/// Returns an array of booleans [len]bool.
pub inline fn lteScalar(self: Self, scalar: anytype) [len]bool {
var res: [len]bool = undefined;
inline for (self.data, 0..) |v, i|
res[i] = (Q{ .value = v }).lte(scalar);
return res;
}
// -------------------------------------------------------------------
// Formatting
// -------------------------------------------------------------------
pub fn formatNumber(
self: Self,
writer: *std.Io.Writer,
@@ -190,16 +527,8 @@ pub fn Vector(comptime len: usize, comptime Q: type) type {
}
test "Format VectorX" {
const MeterPerSecondSq = Scalar(
f32,
Dimensions.init(.{ .L = 1, .T = -2 }),
Scales.init(.{ .T = .n }),
);
const KgMeterPerSecond = Scalar(
f32,
Dimensions.init(.{ .M = 1, .L = 1, .T = -1 }),
Scales.init(.{ .M = .k }),
);
const MeterPerSecondSq = Scalar(f32, .{ .L = 1, .T = -2 }, .{ .T = .n });
const KgMeterPerSecond = Scalar(f32, .{ .M = 1, .L = 1, .T = -1 }, .{ .M = .k });
const accel = MeterPerSecondSq.Vec3.initDefault(9.81);
const momentum = KgMeterPerSecond.Vec3{ .data = .{ 43, 0, 11 } };
@@ -213,7 +542,7 @@ test "Format VectorX" {
}
test "VecX Init and Basic Arithmetic" {
const Meter = Scalar(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Meter = Scalar(i32, .{ .L = 1 }, .{});
const Vec3M = Meter.Vec3;
// Test zero, one, initDefault
@@ -254,15 +583,15 @@ test "VecX Init and Basic Arithmetic" {
}
test "VecX Kinematics (Scalar Mul/Div)" {
const Meter = Scalar(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Scalar(i32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const Meter = Scalar(i32, .{ .L = 1 }, .{});
const Second = Scalar(i32, .{ .T = 1 }, .{});
const Vec3M = Meter.Vec3;
const pos = Vec3M{ .data = .{ 100, 200, 300 } };
const time = Second{ .value = 10 };
// Vector divided by scalar (Velocity = Position / Time)
const vel = pos.divByScalar(time);
const vel = pos.divScalar(time);
try std.testing.expectEqual(10, vel.data[0]);
try std.testing.expectEqual(20, vel.data[1]);
try std.testing.expectEqual(30, vel.data[2]);
@@ -270,7 +599,7 @@ test "VecX Kinematics (Scalar Mul/Div)" {
try std.testing.expectEqual(-1, @TypeOf(vel).dims.get(.T));
// Vector multiplied by scalar (Position = Velocity * Time)
const new_pos = vel.mulByScalar(time);
const new_pos = vel.mulScalar(time);
try std.testing.expectEqual(100, new_pos.data[0]);
try std.testing.expectEqual(200, new_pos.data[1]);
try std.testing.expectEqual(300, new_pos.data[2]);
@@ -279,29 +608,23 @@ test "VecX Kinematics (Scalar Mul/Div)" {
}
test "VecX Element-wise Math and Scaling" {
const Meter = Scalar(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Meter = Scalar(i32, .{ .L = 1 }, .{});
const Vec3M = Meter.Vec3;
const v1 = Vec3M{ .data = .{ 10, 20, 30 } };
const v2 = Vec3M{ .data = .{ 2, 5, 10 } };
// Element-wise division
const div = v1.divBy(v2);
const div = v1.div(v2);
try std.testing.expectEqual(5, div.data[0]);
try std.testing.expectEqual(4, div.data[1]);
try std.testing.expectEqual(3, div.data[2]);
try std.testing.expectEqual(0, @TypeOf(div).dims.get(.L)); // M / M = Dimensionless
// Scale by primitive
const scaled = v1.scale(2);
try std.testing.expectEqual(20, scaled.data[0]);
try std.testing.expectEqual(40, scaled.data[1]);
try std.testing.expectEqual(60, scaled.data[2]);
}
test "VecX Conversions" {
const KiloMeter = Scalar(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const Meter = Scalar(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const KiloMeter = Scalar(i32, .{ .L = 1 }, .{ .L = .k });
const Meter = Scalar(i32, .{ .L = 1 }, .{});
const v_km = KiloMeter.Vec3{ .data = .{ 1, 2, 3 } };
const v_m = v_km.to(Meter);
@@ -316,10 +639,10 @@ test "VecX Conversions" {
}
test "VecX Length" {
const MeterInt = Scalar(i32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const MeterFloat = Scalar(f32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const MeterInt = Scalar(i32, .{ .L = 1 }, .{});
const MeterFloat = Scalar(f32, .{ .L = 1 }, .{});
// Integer length (using your custom isqrt)
// Integer length
// 3-4-5 triangle on XY plane
const v_int = MeterInt.Vec3{ .data = .{ 3, 4, 0 } };
try std.testing.expectEqual(25, v_int.lengthSqr());
@@ -330,3 +653,178 @@ test "VecX Length" {
try std.testing.expectApproxEqAbs(@as(f32, 25.0), v_float.lengthSqr(), 1e-4);
try std.testing.expectApproxEqAbs(@as(f32, 5.0), v_float.length(), 1e-4);
}
test "Vector Comparisons" {
const Meter = Scalar(f32, .{ .L = 1 }, .{});
const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k });
const v1 = Meter.Vec3{ .data = .{ 1000.0, 500.0, 0.0 } };
const v2 = KiloMeter.Vec3{ .data = .{ 1.0, 0.5, 0.0 } };
const v3 = KiloMeter.Vec3{ .data = .{ 1.0, 0.6, 0.0 } };
// 1. Equality (Whole vector)
try std.testing.expect(v1.eqAll(v2));
try std.testing.expect(v1.neAll(v3));
// 2. Element-wise Ordered Comparison
const higher = v3.gt(v1); // compares 1km, 0.6km, 0km vs 1000m, 500m, 0m
try std.testing.expectEqual(false, higher[0]); // 1km == 1000m
try std.testing.expectEqual(true, higher[1]); // 0.6km > 500m
try std.testing.expectEqual(false, higher[2]); // 0 == 0
// 3. Element-wise Equal Comparison
const equal = v3.eq(v1); // compares 1km, 0.6km, 0km vs 1000m, 500m, 0m
try std.testing.expectEqual(true, equal[0]); // 1km == 1000m
try std.testing.expectEqual(false, equal[1]); // 0.6km > 500m
try std.testing.expectEqual(true, equal[2]); // 0 == 0
// 3. Less than or equal
const low_eq = v1.lte(v3);
try std.testing.expect(low_eq[0] and low_eq[1] and low_eq[2]);
}
test "Vector vs Scalar Comparisons" {
const Meter = Scalar(f32, .{ .L = 1 }, .{});
const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k });
const positions = Meter.Vec3{ .data = .{ 500.0, 1200.0, 3000.0 } };
const threshold = KiloMeter{ .value = 1.0 }; // 1km (1000m)
// Check which axes exceed the 1km threshold
const exceeded = positions.gtScalar(threshold);
try std.testing.expectEqual(false, exceeded[0]); // 500m > 1km is false
try std.testing.expectEqual(true, exceeded[1]); // 1200m > 1km is true
try std.testing.expectEqual(true, exceeded[2]); // 3000m > 1km is true
// Check for equality (broadcasted)
const exact_match = positions.eqScalar(Meter{ .value = 500.0 });
try std.testing.expect(exact_match[0] == true);
try std.testing.expect(exact_match[1] == false);
}
test "Vector Dot and Cross Products" {
const Meter = Scalar(f32, .{ .L = 1 }, .{});
const Newton = Scalar(f32, .{ .M = 1, .L = 1, .T = -2 }, .{});
const pos = Meter.Vec3{ .data = .{ 10.0, 0.0, 0.0 } };
const force = Newton.Vec3{ .data = .{ 5.0, 5.0, 0.0 } };
// 1. Dot Product (Work = F dot d)
const work = force.dot(pos);
try std.testing.expectEqual(50.0, work.value);
// Dimensions should be M¹L²T⁻² (Energy/Joules)
try std.testing.expectEqual(1, @TypeOf(work).dims.get(.M));
try std.testing.expectEqual(2, @TypeOf(work).dims.get(.L));
try std.testing.expectEqual(-2, @TypeOf(work).dims.get(.T));
// 2. Cross Product (Torque = r cross F)
const torque = pos.cross(force);
try std.testing.expectEqual(0.0, torque.data[0]);
try std.testing.expectEqual(0.0, torque.data[1]);
try std.testing.expectEqual(50.0, torque.data[2]);
// Torque dimensions are same as Energy but as a Vector
try std.testing.expectEqual(2, @TypeOf(torque).dims.get(.L));
}
test "Vector Abs, Pow, Sqrt and Product" {
const Meter = Scalar(f32, .{ .L = 1 }, .{});
const v1 = Meter.Vec3{ .data = .{ -2.0, 3.0, -4.0 } };
// 1. Abs
const v_abs = v1.abs();
try std.testing.expectEqual(2.0, v_abs.data[0]);
try std.testing.expectEqual(4.0, v_abs.data[2]);
// 2. Product (L1 * L1 * L1 = L3)
const vol = v_abs.product();
try std.testing.expectEqual(24.0, vol.value);
try std.testing.expectEqual(3, @TypeOf(vol).dims.get(.L));
// 3. Pow (Scalar exponent: (L1)^2 = L2)
const area_vec = v_abs.pow(2);
try std.testing.expectEqual(4.0, area_vec.data[0]);
try std.testing.expectEqual(16.0, area_vec.data[2]);
try std.testing.expectEqual(2, @TypeOf(area_vec).dims.get(.L));
// 4. Sqrt
const sqrted = area_vec.sqrt();
try std.testing.expectEqual(2, sqrted.data[0]);
try std.testing.expectEqual(4, sqrted.data[2]);
try std.testing.expectEqual(2, @TypeOf(sqrted).dims.get(.L));
}
test "mulScalar comptime_int" {
const Meter = Scalar(i32, .{ .L = 1 }, .{});
const v = Meter.Vec3{ .data = .{ 1, 2, 3 } };
const scaled = v.mulScalar(10); // comptime_int → dimensionless
try std.testing.expectEqual(10, scaled.data[0]);
try std.testing.expectEqual(20, scaled.data[1]);
try std.testing.expectEqual(30, scaled.data[2]);
// Dimensions unchanged: L¹ × dimensionless = L¹
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
try std.testing.expectEqual(0, @TypeOf(scaled).dims.get(.T));
}
test "mulScalar comptime_float" {
const MeterF = Scalar(f32, .{ .L = 1 }, .{});
const v = MeterF.Vec3{ .data = .{ 1.0, 2.0, 4.0 } };
const scaled = v.mulScalar(0.5); // comptime_float → dimensionless
try std.testing.expectApproxEqAbs(0.5, scaled.data[0], 1e-6);
try std.testing.expectApproxEqAbs(1.0, scaled.data[1], 1e-6);
try std.testing.expectApproxEqAbs(2.0, scaled.data[2], 1e-6);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
}
test "mulScalar T (value type)" {
const MeterF = Scalar(f32, .{ .L = 1 }, .{});
const v = MeterF.Vec3{ .data = .{ 3.0, 6.0, 9.0 } };
const factor: f32 = 2.0;
const scaled = v.mulScalar(factor);
try std.testing.expectApproxEqAbs(6.0, scaled.data[0], 1e-6);
try std.testing.expectApproxEqAbs(12.0, scaled.data[1], 1e-6);
try std.testing.expectApproxEqAbs(18.0, scaled.data[2], 1e-6);
try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L));
}
test "divScalar comptime_int" {
const Meter = Scalar(i32, .{ .L = 1 }, .{});
const v = Meter.Vec3{ .data = .{ 10, 20, 30 } };
const halved = v.divScalar(2); // comptime_int → dimensionless divisor
try std.testing.expectEqual(5, halved.data[0]);
try std.testing.expectEqual(10, halved.data[1]);
try std.testing.expectEqual(15, halved.data[2]);
try std.testing.expectEqual(1, @TypeOf(halved).dims.get(.L));
}
test "divScalar comptime_float" {
const MeterF = Scalar(f64, .{ .L = 1 }, .{});
const v = MeterF.Vec3{ .data = .{ 9.0, 6.0, 3.0 } };
const r = v.divScalar(3.0);
try std.testing.expectApproxEqAbs(3.0, r.data[0], 1e-9);
try std.testing.expectApproxEqAbs(2.0, r.data[1], 1e-9);
try std.testing.expectApproxEqAbs(1.0, r.data[2], 1e-9);
try std.testing.expectEqual(1, @TypeOf(r).dims.get(.L));
}
test "eqScalar / gtScalar with comptime_int on dimensionless vector" {
// Bare numbers are dimensionless, so comparisons only work when vector is dimensionless too.
const DimLess = Scalar(i32, .{}, .{});
const v = DimLess.Vec3{ .data = .{ 1, 2, 3 } };
const eq_res = v.eqScalar(2);
try std.testing.expectEqual(false, eq_res[0]);
try std.testing.expectEqual(true, eq_res[1]);
try std.testing.expectEqual(false, eq_res[2]);
const gt_res = v.gtScalar(1);
try std.testing.expectEqual(false, gt_res[0]);
try std.testing.expectEqual(true, gt_res[1]);
try std.testing.expectEqual(true, gt_res[2]);
}
+73 -55
View File
@@ -1,14 +1,10 @@
const std = @import("std");
const tracy = @import("ztracy");
const Io = std.Io;
const Scalar = @import("Scalar.zig").Scalar;
const Vector = @import("Vector.zig").Vector;
var io: Io = undefined;
pub fn main(init: std.process.Init) !void {
const zone = tracy.ZoneN(@src(), "Main Loop");
defer zone.End();
var stdout_buf: [4096]u8 = undefined;
var stdout_writer: std.Io.File.Writer = .init(.stdout(), init.io, &stdout_buf);
try stdout_writer.interface.print("Starting Benchmarks...", .{});
@@ -23,8 +19,6 @@ pub fn main(init: std.process.Init) !void {
try stdout_writer.flush();
try bench_Vector(&stdout_writer.interface);
try stdout_writer.flush();
tracy.FrameMark();
}
fn getTime() Io.Timestamp {
@@ -84,17 +78,17 @@ fn bench_Scalar(writer: *std.Io.Writer) !void {
\\
, .{ ITERS, SAMPLES });
const Types = .{ i16, i32, i64, i128, i256, f32, f64, f128 };
const TNames = .{ "i16", "i32", "i64", "i128", "i256", "f32", "f64", "f128" };
const Ops = .{ "add", "sub", "mulBy", "divBy", "scale", "to" };
const Types = .{ i16, i32, i64, i128, i256, f32, f64 };
const TNames = .{ "i16", "i32", "i64", "i128", "i256", "f32", "f64" };
const Ops = .{ "add", "sub", "mul", "div", "to", "abs", "pow", "eq", "gt", "mul(n)" };
var results_matrix: [Ops.len][Types.len]f64 = undefined;
comptime var tidx: usize = 0;
inline for (Types, TNames) |T, tname| {
const M = Scalar(T, .init(.{ .L = 1 }), .init(.{}));
const KM = Scalar(T, .init(.{ .L = 1 }), .init(.{ .L = .k }));
const S = Scalar(T, .init(.{ .T = 1 }), .init(.{}));
const M = Scalar(T, .{ .L = 1 }, .{});
const KM = Scalar(T, .{ .L = 1 }, .{ .L = .k });
const S = Scalar(T, .{ .T = 1 }, .{});
inline for (Ops, 0..) |op_name, oidx| {
var samples: [SAMPLES]f64 = undefined;
@@ -109,14 +103,20 @@ fn bench_Scalar(writer: *std.Io.Writer) !void {
(M{ .value = getVal(T, i, 63) }).add(M{ .value = getVal(T, i +% 7, 63) })
else if (comptime std.mem.eql(u8, op_name, "sub"))
(M{ .value = getVal(T, i +% 10, 63) }).sub(M{ .value = getVal(T, i, 63) })
else if (comptime std.mem.eql(u8, op_name, "mulBy"))
(M{ .value = getVal(T, i, 63) }).mulBy(M{ .value = getVal(T, i +% 1, 63) })
else if (comptime std.mem.eql(u8, op_name, "divBy"))
(M{ .value = getVal(T, i +% 10, 63) }).divBy(S{ .value = getVal(T, i, 63) })
else if (comptime std.mem.eql(u8, op_name, "scale"))
(M{ .value = getVal(T, i, 63) }).scale(getVal(T, i +% 2, 63))
else if (comptime std.mem.eql(u8, op_name, "mul"))
(M{ .value = getVal(T, i, 63) }).mul(M{ .value = getVal(T, i +% 1, 63) })
else if (comptime std.mem.eql(u8, op_name, "div"))
(M{ .value = getVal(T, i +% 10, 63) }).div(S{ .value = getVal(T, i, 63) })
else if (comptime std.mem.eql(u8, op_name, "to"))
(KM{ .value = getVal(T, i, 15) }).to(M)
else if (comptime std.mem.eql(u8, op_name, "abs"))
(M{ .value = getVal(T, i, 63) }).abs()
else if (comptime std.mem.eql(u8, op_name, "eq"))
(M{ .value = getVal(T, i, 63) }).eq(M{ .value = getVal(T, i +% 3, 63) })
else if (comptime std.mem.eql(u8, op_name, "gt"))
(M{ .value = getVal(T, i, 63) }).gt(M{ .value = getVal(T, i +% 3, 63) })
else
(KM{ .value = getVal(T, i, 15) }).to(M);
(M{ .value = getVal(T, i, 63) }).mul(3);
},
);
}
@@ -141,9 +141,9 @@ fn bench_Scalar(writer: *std.Io.Writer) !void {
try writer.print("└───────────────────┴──────┴─────────────────────┴─────────────────────┘\n\n", .{});
try writer.print("Median Summary (ns/op):\n", .{});
try writer.print("┌──────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐\n", .{});
try writer.print("│ Operation │ i16 │ i32 │ i64 │ i128 │ i256 │ f32 │ f64 │ f128 │\n", .{});
try writer.print("├──────────────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┤\n", .{});
try writer.print("┌──────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐\n", .{});
try writer.print("│ Operation │ i16 │ i32 │ i64 │ i128 │ i256 │ f32 │ f64 │\n", .{});
try writer.print("├──────────────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┤\n", .{});
inline for (Ops, 0..) |op_name, oidx| {
try writer.print("│ {s:<11} │", .{op_name});
@@ -154,7 +154,7 @@ fn bench_Scalar(writer: *std.Io.Writer) !void {
try writer.print("\n", .{});
}
try writer.print("└──────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘\n", .{});
try writer.print("└──────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘\n", .{});
}
fn bench_vsNative(writer: *std.Io.Writer) !void {
@@ -171,7 +171,7 @@ fn bench_vsNative(writer: *std.Io.Writer) !void {
const Types = .{ i32, i64, i128, f32, f64 };
const TNames = .{ "i32", "i64", "i128", "f32", "f64" };
const Ops = .{ "add", "mulBy", "divBy" };
const Ops = .{ "add", "mul", "div" };
try writer.print(
\\
@@ -188,8 +188,8 @@ fn bench_vsNative(writer: *std.Io.Writer) !void {
var native_total_ns: f64 = 0;
var quantity_total_ns: f64 = 0;
const M = Scalar(T, .init(.{ .L = 1 }), .init(.{}));
const S = Scalar(T, .init(.{ .T = 1 }), .init(.{}));
const M = Scalar(T, .{ .L = 1 }, .{});
const S = Scalar(T, .{ .T = 1 }, .{});
std.mem.doNotOptimizeAway({
for (0..SAMPLES) |_| {
@@ -200,7 +200,7 @@ fn bench_vsNative(writer: *std.Io.Writer) !void {
const b = getValT(T, 2);
_ = if (comptime std.mem.eql(u8, op_name, "add"))
a + b
else if (comptime std.mem.eql(u8, op_name, "mulBy"))
else if (comptime std.mem.eql(u8, op_name, "mul"))
a * b
else if (comptime @typeInfo(T) == .int) @divTrunc(a, b) else a / b;
}
@@ -211,14 +211,14 @@ fn bench_vsNative(writer: *std.Io.Writer) !void {
const q_start = getTime();
for (0..ITERS) |i| {
const qa = M{ .value = getValT(T, i) };
const qb = if (comptime std.mem.eql(u8, op_name, "divBy")) S{ .value = getValT(T, 2) } else M{ .value = getValT(T, 2) };
const qb = if (comptime std.mem.eql(u8, op_name, "div")) S{ .value = getValT(T, 2) } else M{ .value = getValT(T, 2) };
_ = if (comptime std.mem.eql(u8, op_name, "add"))
qa.add(qb)
else if (comptime std.mem.eql(u8, op_name, "mulBy"))
qa.mulBy(qb)
else if (comptime std.mem.eql(u8, op_name, "mul"))
qa.mul(qb)
else
qa.divBy(qb);
qa.div(qb);
}
const q_end = getTime();
quantity_total_ns += @as(f64, @floatFromInt(q_start.durationTo(q_end).toNanoseconds()));
@@ -268,7 +268,7 @@ fn bench_crossTypeVsNative(writer: *std.Io.Writer) !void {
const Types = .{ i16, i64, i128, f32, f64 };
const TNames = .{ "i16", "i64", "i128", "f32", "f64" };
const Ops = .{ "add", "mulBy", "divBy" };
const Ops = .{ "add", "mul", "div" };
try writer.print(
\\
@@ -286,9 +286,9 @@ fn bench_crossTypeVsNative(writer: *std.Io.Writer) !void {
var native_total_ns: f64 = 0;
var quantity_total_ns: f64 = 0;
const M1 = Scalar(T1, .init(.{ .L = 1 }), .init(.{}));
const M2 = Scalar(T2, .init(.{ .L = 1 }), .init(.{}));
const S2 = Scalar(T2, .init(.{ .T = 1 }), .init(.{}));
const M1 = Scalar(T1, .{ .L = 1 }, .{});
const M2 = Scalar(T2, .{ .L = 1 }, .{});
const S2 = Scalar(T2, .{ .T = 1 }, .{});
std.mem.doNotOptimizeAway({
for (0..SAMPLES) |_| {
@@ -301,7 +301,7 @@ fn bench_crossTypeVsNative(writer: *std.Io.Writer) !void {
_ = if (comptime std.mem.eql(u8, op_name, "add"))
a + b
else if (comptime std.mem.eql(u8, op_name, "mulBy"))
else if (comptime std.mem.eql(u8, op_name, "mul"))
a * b
else if (comptime @typeInfo(T1) == .int)
@divTrunc(a, b)
@@ -315,17 +315,17 @@ fn bench_crossTypeVsNative(writer: *std.Io.Writer) !void {
const q_start = getTime();
for (0..ITERS) |i| {
const qa = M1{ .value = getValT(T1, i) };
const qb = if (comptime std.mem.eql(u8, op_name, "divBy"))
const qb = if (comptime std.mem.eql(u8, op_name, "div"))
S2{ .value = getValT(T2, 2) }
else
M2{ .value = getValT(T2, 2) };
_ = if (comptime std.mem.eql(u8, op_name, "add"))
qa.add(qb)
else if (comptime std.mem.eql(u8, op_name, "mulBy"))
qa.mulBy(qb)
else if (comptime std.mem.eql(u8, op_name, "mul"))
qa.mul(qb)
else
qa.divBy(qb);
qa.div(qb);
}
const q_end = getTime();
quantity_total_ns += @as(f64, @floatFromInt(q_start.durationTo(q_end).toNanoseconds()));
@@ -375,28 +375,36 @@ fn bench_Vector(writer: *std.Io.Writer) !void {
try writer.print(
\\
\\ Vector<N, T> benchmark — {d} iterations, {d} samples/cell
\\ (Results in ns/op)
\\ (Results in ns/op; "---" = not applicable for this length)
\\
\\┌─────────────┬──────┬─────────┬─────────┬─────────┐
\\│ Operation │ Type │ Len=3 │ Len=4 │ Len=16 │
\\├─────────────┼──────┼─────────┼─────────┼─────────┤
\\┌──────────────────┬──────┬─────────┬─────────┬─────────┐
\\│ Operation │ Type │ Len=3 │ Len=4 │ Len=16 │
\\├──────────────────┼──────┼─────────┼─────────┼─────────┤
\\
, .{ ITERS, SAMPLES });
const Types = .{ i32, i64, i128, f32, f64 };
const TNames = .{ "i32", "i64", "i128", "f32", "f64" };
const Lengths = .{ 3, 4, 16 };
const Ops = .{ "add", "scale", "mulByScalar", "length" };
// "cross" is only valid for len=3; other cells will show " --- "
const Ops = .{ "add", "div", "mulScalar", "dot", "cross", "product", "pow", "length" };
inline for (Ops, 0..) |op_name, o_idx| {
inline for (Types, TNames) |T, tname| {
try writer.print("│ {s:<11} │ {s:<4} │", .{ op_name, tname });
try writer.print("│ {s:<16} │ {s:<4} │", .{ op_name, tname });
inline for (Lengths) |len| {
const Q_base = Scalar(T, .init(.{ .L = 1 }), .init(.{}));
const Q_time = Scalar(T, .init(.{ .T = 1 }), .init(.{}));
const Q_base = Scalar(T, .{ .L = 1 }, .{});
const Q_time = Scalar(T, .{ .T = 1 }, .{});
const V = Vector(len, Q_base);
// cross product is only defined for len == 3
const is_cross = comptime std.mem.eql(u8, op_name, "cross");
if (comptime is_cross and len != 3) {
try writer.print(" --- │", .{});
continue;
}
var samples: [SAMPLES]f64 = undefined;
std.mem.doNotOptimizeAway({
@@ -408,12 +416,22 @@ fn bench_Vector(writer: *std.Io.Writer) !void {
if (comptime std.mem.eql(u8, op_name, "add")) {
const v2 = V.initDefault(getVal(T, i +% 7, 63));
_ = v1.add(v2);
} else if (comptime std.mem.eql(u8, op_name, "scale")) {
const sc = getVal(T, i +% 2, 63);
_ = v1.scale(sc);
} else if (comptime std.mem.eql(u8, op_name, "mulByScalar")) {
} else if (comptime std.mem.eql(u8, op_name, "div")) {
_ = v1.div(V.initDefault(getVal(T, i +% 2, 63)));
} else if (comptime std.mem.eql(u8, op_name, "mulScalar")) {
const s_val = Q_time{ .value = getVal(T, i +% 2, 63) };
_ = v1.mulByScalar(s_val);
_ = v1.mulScalar(s_val);
} else if (comptime std.mem.eql(u8, op_name, "dot")) {
const v2 = V.initDefault(getVal(T, i +% 5, 63));
_ = v1.dot(v2);
} else if (comptime std.mem.eql(u8, op_name, "cross")) {
// len == 3 guaranteed by the guard above
const v2 = V.initDefault(getVal(T, i +% 5, 63));
_ = v1.cross(v2);
} else if (comptime std.mem.eql(u8, op_name, "product")) {
_ = v1.product();
} else if (comptime std.mem.eql(u8, op_name, "pow")) {
_ = v1.pow(2);
} else if (comptime std.mem.eql(u8, op_name, "length")) {
_ = v1.length();
}
@@ -431,8 +449,8 @@ fn bench_Vector(writer: *std.Io.Writer) !void {
}
if (o_idx < Ops.len - 1) {
try writer.print("├─────────────┼──────┼─────────┼─────────┼─────────┤\n", .{});
try writer.print("├──────────────────┼──────┼─────────┼─────────┼─────────┤\n", .{});
}
}
try writer.print("└─────────────┴──────┴─────────┴─────────┴─────────┘\n", .{});
try writer.print("└──────────────────┴──────┴─────────┴─────────┴─────────┘\n", .{});
}
+66
View File
@@ -30,3 +30,69 @@ pub fn printSuperscript(writer: *std.Io.Writer, n: i32) !void {
try writer.writeAll(s);
}
}
const Scales = @import("Scales.zig");
const Dimensions = @import("Dimensions.zig");
const Dimension = @import("Dimensions.zig").Dimension;
pub fn finerScales(comptime T1: type, comptime T2: type) Scales {
const d1: Dimensions = T1.dims;
const d2: Dimensions = T2.dims;
const s1: Scales = T1.scales;
const s2: Scales = T2.scales;
comptime var out = Scales.initFill(.none);
inline for (std.enums.values(Dimension)) |dim| {
const scale1 = comptime s1.get(dim);
const scale2 = comptime s2.get(dim);
out.set(dim, if (comptime d1.get(dim) == 0 and d2.get(dim) == 0)
.none
else if (comptime d1.get(dim) == 0)
scale2
else if (comptime d2.get(dim) == 0)
scale1
else if (comptime scale1.getFactor() > scale2.getFactor())
scale2
else
scale1);
}
comptime return out;
}
// ---------------------------------------------------------------------------
// RHS normalisation helpers
// ---------------------------------------------------------------------------
const Scalar = @import("Scalar.zig").Scalar;
/// Returns true if `T` is a `Scalar_` type (has `dims`, `scales`, and `value`).
pub fn isScalarType(comptime T: type) bool {
return @typeInfo(T) == .@"struct" and
@hasDecl(T, "dims") and
@hasDecl(T, "scales") and
@hasField(T, "value");
}
/// Resolve the Scalar type that `rhs` will be treated as.
///
/// Accepted rhs types:
/// - Any `Scalar_` type → returned as-is
/// - `comptime_int` / `comptime_float` → dimensionless `Scalar_(BaseT, {}, {})`
/// - `BaseT` (the scalar's value type) → dimensionless `Scalar_(BaseT, {}, {})`
///
/// Everything else is a compile error, including other int/float types.
pub fn rhsScalarType(comptime BaseT: type, comptime RhsT: type) type {
if (comptime isScalarType(RhsT)) return RhsT;
if (comptime RhsT == comptime_int or RhsT == comptime_float or RhsT == BaseT)
return Scalar(BaseT, .{}, .{});
@compileError(
"rhs must be a Scalar, " ++ @typeName(BaseT) ++
", comptime_int, or comptime_float; got " ++ @typeName(RhsT),
);
}
/// Convert `rhs` to its normalised Scalar form (see `rhsScalarType`).
pub inline fn toRhsScalar(comptime BaseT: type, rhs: anytype) rhsScalarType(BaseT, @TypeOf(rhs)) {
if (comptime isScalarType(@TypeOf(rhs))) return rhs;
const DimLess = Scalar(BaseT, .{}, .{});
return DimLess{ .value = @as(BaseT, rhs) };
}